opencode-worktree 0.3.4 → 0.4.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/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
 
@@ -8,18 +8,21 @@ Terminal UI for managing git worktrees and launching `opencode` in the selected
8
8
  - Worktree metadata display: last edited time, dirty status, remote tracking
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
+ - **Create branch from worktree**: create a new branch from any worktree's current commit, with optional checkout
11
12
  - Post-create hooks: automatically run commands (e.g., `npm install`) after creating a worktree
12
- - Open worktree folder in file manager
13
+ - Open worktree folder in file manager or custom editor
13
14
  - Unlink worktrees (remove directory, keep branch)
14
15
  - Delete worktrees and local branches (never remote)
15
16
  - Multi-select delete mode for batch deletion
16
- - Launches `opencode` in the selected worktree
17
+ - **Customizable launch command**: use `opencode`, `cursor`, `claude`, `code`, or any CLI tool
18
+ - **Global configuration**: settings stored in `~/.config/opencode-worktree/config.json` with per-repo overrides
17
19
  - Refresh list on demand
20
+ - Update notifications when new versions are available
18
21
 
19
22
  ## Requirements
20
23
 
21
24
  - Git
22
- - `opencode` available on your PATH
25
+ - A CLI tool available on your PATH (e.g., `opencode`, `cursor`, `claude`, `code`)
23
26
 
24
27
  ## Install (npm)
25
28
 
@@ -42,14 +45,22 @@ opencode-worktree /path/to/your/repo
42
45
  ## Keybindings
43
46
 
44
47
  - `Up`/`Down` or `j`/`k`: navigate
45
- - `Enter`: open selected worktree in opencode (or toggle selection in delete mode)
48
+ - `Enter`: open selected worktree in configured tool (or toggle selection in delete mode)
46
49
  - `o`: open worktree folder in file manager or custom editor (configurable)
47
50
  - `d`: enter multi-select delete mode (press again to confirm deletion)
48
51
  - `n`: create new worktree
49
- - `c`: edit configuration (post-create hooks)
52
+ - `b`: create a new branch from selected worktree's current commit
53
+ - `c`: edit configuration (hooks, open command, launch command)
50
54
  - `r`: refresh list
51
55
  - `q` or `Esc`: quit (or cancel dialogs/modes)
52
56
 
57
+ ### Create branch from worktree
58
+
59
+ 1. Select a worktree and press `b`
60
+ 2. Enter a name for the new branch
61
+ 3. The branch is created starting from the worktree's current commit
62
+ 4. Choose whether to checkout the new branch in the worktree
63
+
53
64
  ### Multi-select delete mode
54
65
 
55
66
  1. Press `d` to enter selection mode
@@ -59,73 +70,92 @@ opencode-worktree /path/to/your/repo
59
70
 
60
71
  ## Configuration
61
72
 
62
- You can configure per-repository settings by creating a `.opencode-worktree.json` file in your repository root.
73
+ Configuration is stored globally at `~/.config/opencode-worktree/config.json` with support for default settings and per-repository overrides. Press `c` in the TUI to edit settings.
63
74
 
64
- ### First-time setup
75
+ Repositories are identified by their git remote URL (e.g., `github.com/user/repo`).
65
76
 
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`.
77
+ ### Configuration structure
67
78
 
68
- ### Editing configuration
79
+ ```json
80
+ {
81
+ "default": {
82
+ "postCreateHook": "",
83
+ "openCommand": "",
84
+ "launchCommand": "opencode"
85
+ },
86
+ "repos": {
87
+ "github.com/user/repo": {
88
+ "postCreateHook": "npm install",
89
+ "launchCommand": "cursor"
90
+ }
91
+ }
92
+ }
93
+ ```
69
94
 
70
- Press `c` at any time to edit your configuration. You can configure:
95
+ ### Configuration options
71
96
 
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`)
97
+ | Option | Description | Default |
98
+ |--------|-------------|---------|
99
+ | `postCreateHook` | Command to run after creating a worktree | none |
100
+ | `openCommand` | Command for opening worktree folders (`o` key) | system default |
101
+ | `launchCommand` | Command to launch when selecting a worktree (`Enter` key) | `opencode` |
74
102
 
75
- ### Post-create hooks
103
+ ### Example per-repo configuration
76
104
 
77
- Run a command automatically after creating a new worktree. Useful for installing dependencies.
105
+ When you edit config in the TUI, settings are saved under the repo's key (derived from git remote URL):
78
106
 
79
107
  ```json
80
108
  {
81
- "postCreateHook": "npm install"
109
+ "default": {
110
+ "launchCommand": "opencode"
111
+ },
112
+ "repos": {
113
+ "github.com/myorg/frontend": {
114
+ "postCreateHook": "npm install",
115
+ "openCommand": "code",
116
+ "launchCommand": "cursor"
117
+ },
118
+ "github.com/myorg/backend": {
119
+ "postCreateHook": "go mod download",
120
+ "launchCommand": "zed"
121
+ }
122
+ }
82
123
  }
83
124
  ```
84
125
 
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.
126
+ ### Editing configuration
86
127
 
87
- **Examples:**
128
+ Press `c` at any time to edit your configuration. The config editor title shows which repository you're configuring (e.g., "Config: github.com/user/repo"). Use `Tab` to switch between fields.
88
129
 
89
- ```json
90
- {
91
- "postCreateHook": "bun install"
92
- }
93
- ```
130
+ **Note:** Repositories without a git remote will use default settings. The TUI shows a warning when editing config for repos without a remote.
94
131
 
95
- ```json
96
- {
97
- "postCreateHook": "npm install && npm run setup"
98
- }
99
- ```
132
+ ### Post-create hooks
133
+
134
+ Run a command automatically after creating a new worktree. Useful for installing dependencies.
135
+
136
+ 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.
137
+
138
+ **Examples:** `npm install`, `bun install`, `npm install && npm run setup`
100
139
 
101
140
  ### Custom open command
102
141
 
103
142
  Use a custom command when pressing `o` to open worktree folders. Useful for opening in your preferred IDE.
104
143
 
105
- ```json
106
- {
107
- "openCommand": "webstorm"
108
- }
109
- ```
144
+ **Examples:** `code`, `webstorm`, `idea`
110
145
 
111
- **Examples:**
146
+ ### Custom launch command
112
147
 
113
- ```json
114
- {
115
- "openCommand": "code"
116
- }
117
- ```
148
+ Use a different tool instead of `opencode` when pressing `Enter` to open a worktree. This allows you to use any CLI-based coding tool.
118
149
 
119
- ```json
120
- {
121
- "postCreateHook": "npm install",
122
- "openCommand": "webstorm"
123
- }
124
- ```
150
+ **Examples:** `cursor`, `claude`, `code`, `zed`
151
+
152
+ ### Migration from v0.3.x
153
+
154
+ Previous versions stored config in `.opencode-worktree.json` files in each repository. These files are now ignored. Your settings will need to be reconfigured via the TUI (`c` key), which will save them to the new global config location.
125
155
 
126
156
  ## Update notifications
127
157
 
128
- When a new version is published to npm, the CLI will show a non-intrusive update message on the next run.
158
+ 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
159
 
130
160
  ## Development
131
161
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-worktree",
3
- "version": "0.3.4",
3
+ "version": "0.4.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "TUI for managing git worktrees with opencode integration.",
package/src/config.ts CHANGED
@@ -1,74 +1,213 @@
1
- import { readFileSync, writeFileSync, existsSync } from "node:fs";
1
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
2
2
  import { join } from "node:path";
3
+ import { homedir } from "node:os";
4
+ import { getRepoKey } from "./git.js";
5
+ import type { Config, GlobalConfig, LoadRepoConfigResult } from "./types.js";
3
6
 
4
- export type Config = {
5
- postCreateHook?: string;
6
- openCommand?: string; // Custom command to open worktree folder (e.g., "webstorm", "code")
7
+ // Re-export Config type for backwards compatibility
8
+ export type { Config } from "./types.js";
9
+
10
+ const CONFIG_DIR = join(homedir(), ".config", "opencode-worktree");
11
+ const CONFIG_FILE = join(CONFIG_DIR, "config.json");
12
+
13
+ /**
14
+ * Get the path to the global config directory
15
+ */
16
+ export const getGlobalConfigDir = (): string => {
17
+ return CONFIG_DIR;
7
18
  };
8
19
 
9
- const CONFIG_FILENAME = ".opencode-worktree.json";
20
+ /**
21
+ * Get the path to the global config file
22
+ */
23
+ export const getGlobalConfigPath = (): string => {
24
+ return CONFIG_FILE;
25
+ };
10
26
 
11
27
  /**
12
- * Get the path to the config file for a repo
28
+ * Get the default configuration values
13
29
  */
14
- export const getConfigPath = (repoRoot: string): string => {
15
- return join(repoRoot, CONFIG_FILENAME);
30
+ export const getDefaultConfig = (): Config => {
31
+ return {
32
+ postCreateHook: "",
33
+ openCommand: "",
34
+ launchCommand: "opencode",
35
+ };
16
36
  };
17
37
 
18
38
  /**
19
- * Check if a config file exists for the repo
39
+ * Create an empty global config structure
20
40
  */
21
- export const configExists = (repoRoot: string): boolean => {
22
- return existsSync(getConfigPath(repoRoot));
41
+ const createEmptyGlobalConfig = (): GlobalConfig => {
42
+ return {
43
+ default: getDefaultConfig(),
44
+ repos: {},
45
+ };
23
46
  };
24
47
 
25
48
  /**
26
- * Load per-repo configuration from .opencode-worktree.json in the repo root
49
+ * Ensure the config directory exists
27
50
  */
28
- export const loadRepoConfig = (repoRoot: string): Config => {
29
- const configPath = getConfigPath(repoRoot);
51
+ const ensureConfigDir = (): boolean => {
52
+ try {
53
+ if (!existsSync(CONFIG_DIR)) {
54
+ mkdirSync(CONFIG_DIR, { recursive: true });
55
+ }
56
+ return true;
57
+ } catch {
58
+ return false;
59
+ }
60
+ };
30
61
 
31
- if (!existsSync(configPath)) {
32
- return {};
62
+ /**
63
+ * Load the entire global config file
64
+ */
65
+ export const loadGlobalConfig = (): GlobalConfig => {
66
+ if (!existsSync(CONFIG_FILE)) {
67
+ return createEmptyGlobalConfig();
33
68
  }
34
69
 
35
70
  try {
36
- const content = readFileSync(configPath, "utf8");
71
+ const content = readFileSync(CONFIG_FILE, "utf8");
37
72
  const parsed = JSON.parse(content);
38
73
 
39
- // Validate the config structure
74
+ // Validate and normalize the config structure
40
75
  if (typeof parsed !== "object" || parsed === null) {
41
- return {};
76
+ return createEmptyGlobalConfig();
42
77
  }
43
78
 
44
- const config: Config = {};
79
+ const globalConfig: GlobalConfig = {
80
+ default: { ...getDefaultConfig() },
81
+ repos: {},
82
+ };
45
83
 
46
- if (typeof parsed.postCreateHook === "string") {
47
- config.postCreateHook = parsed.postCreateHook;
84
+ // Parse default config
85
+ if (typeof parsed.default === "object" && parsed.default !== null) {
86
+ if (typeof parsed.default.postCreateHook === "string") {
87
+ globalConfig.default.postCreateHook = parsed.default.postCreateHook;
88
+ }
89
+ if (typeof parsed.default.openCommand === "string") {
90
+ globalConfig.default.openCommand = parsed.default.openCommand;
91
+ }
92
+ if (typeof parsed.default.launchCommand === "string") {
93
+ globalConfig.default.launchCommand = parsed.default.launchCommand;
94
+ }
48
95
  }
49
96
 
50
- if (typeof parsed.openCommand === "string") {
51
- config.openCommand = parsed.openCommand;
97
+ // Parse repos config
98
+ if (typeof parsed.repos === "object" && parsed.repos !== null) {
99
+ for (const [key, value] of Object.entries(parsed.repos)) {
100
+ if (typeof value === "object" && value !== null) {
101
+ const repoConfig: Partial<Config> = {};
102
+ const v = value as Record<string, unknown>;
103
+
104
+ if (typeof v.postCreateHook === "string") {
105
+ repoConfig.postCreateHook = v.postCreateHook;
106
+ }
107
+ if (typeof v.openCommand === "string") {
108
+ repoConfig.openCommand = v.openCommand;
109
+ }
110
+ if (typeof v.launchCommand === "string") {
111
+ repoConfig.launchCommand = v.launchCommand;
112
+ }
113
+
114
+ // Only add if there are actual values
115
+ if (Object.keys(repoConfig).length > 0) {
116
+ globalConfig.repos[key] = repoConfig;
117
+ }
118
+ }
119
+ }
52
120
  }
53
121
 
54
- return config;
122
+ return globalConfig;
55
123
  } catch {
56
124
  // If we can't read or parse the config, return empty
57
- return {};
125
+ return createEmptyGlobalConfig();
58
126
  }
59
127
  };
60
128
 
61
129
  /**
62
- * Save configuration to .opencode-worktree.json in the repo root
130
+ * Save the entire global config file
63
131
  */
64
- export const saveRepoConfig = (repoRoot: string, config: Config): boolean => {
65
- const configPath = getConfigPath(repoRoot);
132
+ export const saveGlobalConfig = (config: GlobalConfig): boolean => {
133
+ if (!ensureConfigDir()) {
134
+ return false;
135
+ }
66
136
 
67
137
  try {
68
138
  const content = JSON.stringify(config, null, 2) + "\n";
69
- writeFileSync(configPath, content, "utf8");
139
+ writeFileSync(CONFIG_FILE, content, "utf8");
70
140
  return true;
71
141
  } catch {
72
142
  return false;
73
143
  }
74
144
  };
145
+
146
+ /**
147
+ * Load configuration for a specific repository
148
+ * Merges default config with repo-specific overrides
149
+ * Returns the config and the repo key (null if no remote)
150
+ */
151
+ export const loadRepoConfig = (repoRoot: string): LoadRepoConfigResult => {
152
+ const repoKey = getRepoKey(repoRoot);
153
+ const globalConfig = loadGlobalConfig();
154
+
155
+ // Start with default config
156
+ const config: Config = { ...globalConfig.default };
157
+
158
+ // If we have a repo key, merge in repo-specific config
159
+ if (repoKey && globalConfig.repos[repoKey]) {
160
+ const repoConfig = globalConfig.repos[repoKey];
161
+
162
+ if (repoConfig.postCreateHook !== undefined) {
163
+ config.postCreateHook = repoConfig.postCreateHook;
164
+ }
165
+ if (repoConfig.openCommand !== undefined) {
166
+ config.openCommand = repoConfig.openCommand;
167
+ }
168
+ if (repoConfig.launchCommand !== undefined) {
169
+ config.launchCommand = repoConfig.launchCommand;
170
+ }
171
+ }
172
+
173
+ return { config, repoKey };
174
+ };
175
+
176
+ /**
177
+ * Save configuration for a specific repository
178
+ * Only saves values that differ from the default
179
+ * Returns false if there's no remote (can't save repo-specific config)
180
+ */
181
+ export const saveRepoConfig = (repoRoot: string, config: Config): boolean => {
182
+ const repoKey = getRepoKey(repoRoot);
183
+
184
+ if (!repoKey) {
185
+ // No remote - can't save repo-specific config
186
+ return false;
187
+ }
188
+
189
+ const globalConfig = loadGlobalConfig();
190
+
191
+ // Calculate which values differ from default
192
+ const repoConfig: Partial<Config> = {};
193
+
194
+ if (config.postCreateHook !== globalConfig.default.postCreateHook) {
195
+ repoConfig.postCreateHook = config.postCreateHook;
196
+ }
197
+ if (config.openCommand !== globalConfig.default.openCommand) {
198
+ repoConfig.openCommand = config.openCommand;
199
+ }
200
+ if (config.launchCommand !== globalConfig.default.launchCommand) {
201
+ repoConfig.launchCommand = config.launchCommand;
202
+ }
203
+
204
+ // Update or remove the repo entry
205
+ if (Object.keys(repoConfig).length > 0) {
206
+ globalConfig.repos[repoKey] = repoConfig;
207
+ } else {
208
+ // All values match default, remove the entry
209
+ delete globalConfig.repos[repoKey];
210
+ }
211
+
212
+ return saveGlobalConfig(globalConfig);
213
+ };
package/src/git.ts CHANGED
@@ -1,6 +1,71 @@
1
1
  import { execFileSync } from "node:child_process";
2
2
  import { WorktreeInfo } from "./types.js";
3
3
 
4
+ /**
5
+ * Get the remote origin URL for a repository
6
+ */
7
+ export const getRemoteOriginUrl = (repoRoot: string): string | null => {
8
+ try {
9
+ const output = execFileSync("git", ["remote", "get-url", "origin"], {
10
+ cwd: repoRoot,
11
+ stdio: ["ignore", "pipe", "ignore"],
12
+ encoding: "utf8",
13
+ });
14
+ return output.trim() || null;
15
+ } catch {
16
+ return null;
17
+ }
18
+ };
19
+
20
+ /**
21
+ * Normalize a git remote URL to a consistent key format
22
+ * Examples:
23
+ * git@github.com:user/repo.git → github.com/user/repo
24
+ * https://github.com/user/repo.git → github.com/user/repo
25
+ * ssh://git@github.com/user/repo.git → github.com/user/repo
26
+ */
27
+ export const normalizeRemoteUrl = (url: string): string => {
28
+ let normalized = url.trim();
29
+
30
+ // Remove .git suffix
31
+ if (normalized.endsWith(".git")) {
32
+ normalized = normalized.slice(0, -4);
33
+ }
34
+
35
+ // Handle SSH format: git@github.com:user/repo
36
+ const sshMatch = normalized.match(/^git@([^:]+):(.+)$/);
37
+ if (sshMatch) {
38
+ return `${sshMatch[1]}/${sshMatch[2]}`;
39
+ }
40
+
41
+ // Handle SSH URL format: ssh://git@github.com/user/repo
42
+ const sshUrlMatch = normalized.match(/^ssh:\/\/git@([^/]+)\/(.+)$/);
43
+ if (sshUrlMatch) {
44
+ return `${sshUrlMatch[1]}/${sshUrlMatch[2]}`;
45
+ }
46
+
47
+ // Handle HTTPS format: https://github.com/user/repo
48
+ const httpsMatch = normalized.match(/^https?:\/\/([^/]+)\/(.+)$/);
49
+ if (httpsMatch) {
50
+ return `${httpsMatch[1]}/${httpsMatch[2]}`;
51
+ }
52
+
53
+ // Fallback: return as-is (shouldn't happen for valid URLs)
54
+ return normalized;
55
+ };
56
+
57
+ /**
58
+ * Get the normalized repo key for a repository (for config lookup)
59
+ * Returns null if no remote origin is configured
60
+ */
61
+ export const getRepoKey = (repoRoot: string): string | null => {
62
+ const remoteUrl = getRemoteOriginUrl(repoRoot);
63
+ if (!remoteUrl) {
64
+ return null;
65
+ }
66
+ return normalizeRemoteUrl(remoteUrl);
67
+ };
68
+
4
69
  export const resolveRepoRoot = (cwd: string): string | null => {
5
70
  try {
6
71
  const output = execFileSync("git", ["rev-parse", "--show-toplevel"], {
@@ -280,3 +345,69 @@ export const deleteWorktree = (
280
345
  return { success: false, error, step: "branch" };
281
346
  }
282
347
  };
348
+
349
+ export type CreateBranchResult =
350
+ | { success: true }
351
+ | { success: false; error: string };
352
+
353
+ /**
354
+ * Create a new branch from a specific commit
355
+ */
356
+ export const createBranchFromCommit = (
357
+ repoRoot: string,
358
+ branchName: string,
359
+ commitHash: string,
360
+ ): CreateBranchResult => {
361
+ try {
362
+ execFileSync("git", ["branch", branchName, commitHash], {
363
+ cwd: repoRoot,
364
+ stdio: ["ignore", "pipe", "pipe"],
365
+ encoding: "utf8",
366
+ });
367
+ return { success: true };
368
+ } catch (e) {
369
+ const error = e instanceof Error ? e.message : String(e);
370
+ return { success: false, error };
371
+ }
372
+ };
373
+
374
+ export type CheckoutResult =
375
+ | { success: true }
376
+ | { success: false; error: string };
377
+
378
+ /**
379
+ * Checkout a branch in a specific worktree
380
+ * Note: This changes the branch the worktree is tracking
381
+ */
382
+ export const checkoutBranch = (
383
+ worktreePath: string,
384
+ branchName: string,
385
+ ): CheckoutResult => {
386
+ try {
387
+ execFileSync("git", ["checkout", branchName], {
388
+ cwd: worktreePath,
389
+ stdio: ["ignore", "pipe", "pipe"],
390
+ encoding: "utf8",
391
+ });
392
+ return { success: true };
393
+ } catch (e) {
394
+ const error = e instanceof Error ? e.message : String(e);
395
+ return { success: false, error };
396
+ }
397
+ };
398
+
399
+ /**
400
+ * Get the current HEAD commit hash for a worktree
401
+ */
402
+ export const getHeadCommit = (worktreePath: string): string | null => {
403
+ try {
404
+ const output = execFileSync("git", ["rev-parse", "HEAD"], {
405
+ cwd: worktreePath,
406
+ stdio: ["ignore", "pipe", "ignore"],
407
+ encoding: "utf8",
408
+ });
409
+ return output.trim() || null;
410
+ } catch {
411
+ return null;
412
+ }
413
+ };
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/types.ts CHANGED
@@ -8,3 +8,29 @@ export type WorktreeInfo = {
8
8
  isOnRemote: boolean;
9
9
  lastModified: Date | null;
10
10
  };
11
+
12
+ /**
13
+ * Per-repo configuration options
14
+ */
15
+ export type Config = {
16
+ postCreateHook?: string;
17
+ openCommand?: string; // Custom command to open worktree folder (e.g., "webstorm", "code")
18
+ launchCommand?: string; // Custom command to launch instead of opencode (e.g., "cursor", "claude")
19
+ };
20
+
21
+ /**
22
+ * Global configuration structure stored at ~/.config/opencode-worktree/config.json
23
+ * Contains default settings and per-repo overrides keyed by normalized git remote URL
24
+ */
25
+ export type GlobalConfig = {
26
+ default: Config;
27
+ repos: Record<string, Partial<Config>>;
28
+ };
29
+
30
+ /**
31
+ * Result of loading repo config, includes the repo key for display purposes
32
+ */
33
+ export type LoadRepoConfigResult = {
34
+ config: Config;
35
+ repoKey: string | null; // null means no remote origin configured
36
+ };