Skip to content

Commit d58e253

Browse files
committed
feat: add post-create hooks and config editor (v0.3.0)
- Add post-create hooks to run commands after creating worktrees (e.g., npm install) - Add config editor (press 'c') to edit hooks from the TUI - Add first-time setup prompt when no config exists - Stream hook output to TUI in real-time - Handle hook failures with option to continue or cancel - Add .opencode-worktree.json config file support - Update keybindings and documentation
1 parent 4ceb002 commit d58e253

7 files changed

Lines changed: 578 additions & 13 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ node_modules
22
dist
33
.DS_Store
44
*.log
5+
.opencode-worktree.json

README.md

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ Terminal UI for managing git worktrees and launching `opencode` in the selected
77
- Lists all worktrees with branch, path, and metadata
88
- Worktree metadata display: last edited time, dirty status, remote tracking
99
- Status indicators: `[main]` for main worktree, `[*]` for uncommitted changes, `[local]` for local-only branches
10-
- Create new worktrees directly from the TUI (returns to list with new worktree preselected)
10+
- Create new worktrees directly from the TUI
11+
- Post-create hooks: automatically run commands (e.g., `npm install`) after creating a worktree
1112
- Open worktree folder in file manager
1213
- Unlink worktrees (remove directory, keep branch)
1314
- Delete worktrees and local branches (never remote)
@@ -45,6 +46,7 @@ opencode-worktree /path/to/your/repo
4546
- `o`: open worktree folder in file manager (Finder/Explorer)
4647
- `d`: enter multi-select delete mode (press again to confirm deletion)
4748
- `n`: create new worktree
49+
- `c`: edit configuration (post-create hooks)
4850
- `r`: refresh list
4951
- `q` or `Esc`: quit (or cancel dialogs/modes)
5052

@@ -55,6 +57,44 @@ opencode-worktree /path/to/your/repo
5557
3. Press `d` again to confirm and choose unlink/delete action
5658
4. Press `Esc` to cancel and return to normal mode
5759

60+
## Configuration
61+
62+
You can configure per-repository settings by creating a `.opencode-worktree.json` file in your repository root.
63+
64+
### First-time setup
65+
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`.
67+
68+
### Editing configuration
69+
70+
Press `c` at any time to edit your configuration. Currently, this allows you to set or modify the post-create hook command.
71+
72+
### Post-create hooks
73+
74+
Run a command automatically after creating a new worktree. Useful for installing dependencies.
75+
76+
```json
77+
{
78+
"postCreateHook": "npm install"
79+
}
80+
```
81+
82+
The hook output is streamed to the TUI in real-time. If the hook fails, you can choose to open opencode anyway or cancel.
83+
84+
**Examples:**
85+
86+
```json
87+
{
88+
"postCreateHook": "bun install"
89+
}
90+
```
91+
92+
```json
93+
{
94+
"postCreateHook": "npm install && npm run setup"
95+
}
96+
```
97+
5898
## Update notifications
5999

60100
When a new version is published to npm, the CLI will show a non-intrusive update message on the next run.

ROADMAP.md

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,10 @@
2121

2222
## v0.3 - Enhanced Navigation & Configuration
2323

24-
### Navigation
25-
26-
- [ ] **Switch between worktrees** - Open a new terminal pane in the selected worktree instead of launching opencode
27-
2824
### Configuration
2925

30-
- [ ] **Post-create hooks** - Run custom commands after creating a worktree (e.g., `npm install`)
26+
- [x] **Post-create hooks** - Run custom commands after creating a worktree (e.g., `npm install`) with streaming output and failure handling
27+
- [x] **Config editor** - Press `c` to edit configuration, with first-time setup prompt
3128

3229
---
3330

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "opencode-worktree",
3-
"version": "0.2.9",
3+
"version": "0.3.0",
44
"private": false,
55
"type": "module",
66
"description": "TUI for managing git worktrees with opencode integration.",

src/config.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { readFileSync, writeFileSync, existsSync } from "node:fs";
2+
import { join } from "node:path";
3+
4+
export type Config = {
5+
postCreateHook?: string;
6+
};
7+
8+
const CONFIG_FILENAME = ".opencode-worktree.json";
9+
10+
/**
11+
* Get the path to the config file for a repo
12+
*/
13+
export const getConfigPath = (repoRoot: string): string => {
14+
return join(repoRoot, CONFIG_FILENAME);
15+
};
16+
17+
/**
18+
* Check if a config file exists for the repo
19+
*/
20+
export const configExists = (repoRoot: string): boolean => {
21+
return existsSync(getConfigPath(repoRoot));
22+
};
23+
24+
/**
25+
* Load per-repo configuration from .opencode-worktree.json in the repo root
26+
*/
27+
export const loadRepoConfig = (repoRoot: string): Config => {
28+
const configPath = getConfigPath(repoRoot);
29+
30+
if (!existsSync(configPath)) {
31+
return {};
32+
}
33+
34+
try {
35+
const content = readFileSync(configPath, "utf8");
36+
const parsed = JSON.parse(content);
37+
38+
// Validate the config structure
39+
if (typeof parsed !== "object" || parsed === null) {
40+
return {};
41+
}
42+
43+
const config: Config = {};
44+
45+
if (typeof parsed.postCreateHook === "string") {
46+
config.postCreateHook = parsed.postCreateHook;
47+
}
48+
49+
return config;
50+
} catch {
51+
// If we can't read or parse the config, return empty
52+
return {};
53+
}
54+
};
55+
56+
/**
57+
* Save configuration to .opencode-worktree.json in the repo root
58+
*/
59+
export const saveRepoConfig = (repoRoot: string, config: Config): boolean => {
60+
const configPath = getConfigPath(repoRoot);
61+
62+
try {
63+
const content = JSON.stringify(config, null, 2) + "\n";
64+
writeFileSync(configPath, content, "utf8");
65+
return true;
66+
} catch {
67+
return false;
68+
}
69+
};

src/hooks.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { spawn } from "node:child_process";
2+
3+
export type HookResult = {
4+
success: boolean;
5+
exitCode: number | null;
6+
};
7+
8+
export type HookCallbacks = {
9+
onOutput: (data: string) => void;
10+
onComplete: (result: HookResult) => void;
11+
};
12+
13+
/**
14+
* Run a post-create hook command with streaming output
15+
* Returns a function to abort the hook if needed
16+
*/
17+
export const runPostCreateHook = (
18+
worktreePath: string,
19+
command: string,
20+
callbacks: HookCallbacks
21+
): (() => void) => {
22+
const shell = process.platform === "win32" ? "cmd" : "/bin/sh";
23+
const shellFlag = process.platform === "win32" ? "/c" : "-c";
24+
25+
const child = spawn(shell, [shellFlag, command], {
26+
cwd: worktreePath,
27+
stdio: ["ignore", "pipe", "pipe"],
28+
env: { ...process.env },
29+
});
30+
31+
// Stream stdout
32+
child.stdout?.on("data", (data: Buffer) => {
33+
callbacks.onOutput(data.toString());
34+
});
35+
36+
// Stream stderr
37+
child.stderr?.on("data", (data: Buffer) => {
38+
callbacks.onOutput(data.toString());
39+
});
40+
41+
// Handle completion
42+
child.on("close", (code: number | null) => {
43+
callbacks.onComplete({
44+
success: code === 0,
45+
exitCode: code,
46+
});
47+
});
48+
49+
// Handle errors
50+
child.on("error", (err: Error) => {
51+
callbacks.onOutput(`Error: ${err.message}\n`);
52+
callbacks.onComplete({
53+
success: false,
54+
exitCode: null,
55+
});
56+
});
57+
58+
// Return abort function
59+
return () => {
60+
child.kill("SIGTERM");
61+
};
62+
};

0 commit comments

Comments
 (0)