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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # opencode-worktree
2
2
 
3
- Terminal UI for managing git worktrees and launching `opencode` in the selected worktree.
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
- - Launches `opencode` in the selected worktree
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
- - `opencode` available on your PATH
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 opencode (or toggle selection in delete mode)
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 (post-create hooks)
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 a post-create hook. You can also skip this step and configure it later by pressing `c`.
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. You can configure:
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 opencode anyway or cancel.
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": "webstorm"
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 CLI will show a non-intrusive update message on the next run.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-worktree",
3
- "version": "0.3.4",
3
+ "version": "0.3.5",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "TUI for managing git worktrees with opencode integration.",
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
- export const isOpenCodeAvailable = (): boolean => {
4
- const result = spawnSync("opencode", ["--version"], {
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
- export const launchOpenCode = (cwd: string): void => {
11
- const child = spawn("opencode", [], {
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 { isOpenCodeAvailable, launchOpenCode, openInFileManager } from "./opencode.js";
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 configActiveField: "hook" | "open" = "hook";
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.opencodeAvailable = isOpenCodeAvailable();
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
- // Switch between fields
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.configOpenInput?.blur();
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("opencode is not available on PATH.", "error");
400
+ this.setStatus(`${cmdName} is not available on PATH.`, "error");
393
401
  return;
394
402
  }
395
403
 
396
404
  this.cleanup(false);
397
- launchOpenCode(worktree.path);
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 opencode directly
532
+ // No hook, launch command directly
525
533
  this.hideCreateWorktreeInput();
526
534
  this.cleanup(false);
527
- launchOpenCode(result.path);
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 opencode
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
- launchOpenCode(this.pendingWorktreePath);
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
- launchOpenCode(this.pendingWorktreePath);
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: 12,
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., webstorm, code):",
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: 7,
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.opencodeAvailable = isOpenCodeAvailable();
973
+ const cmdName = this.repoConfig.launchCommand || "opencode";
974
+ this.opencodeAvailable = isCommandAvailable(cmdName);
926
975
  if (!this.opencodeAvailable) {
927
- this.setStatus("opencode is not available on PATH.", "error");
976
+ this.setStatus(`${cmdName} is not available on PATH.`, "error");
928
977
  }
929
978
 
930
979
  this.renderer.requestRender();