opencode-gitwt 0.2.1 → 0.3.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 +26 -3
- package/package.json +3 -4
- package/src/index.js +121 -60
package/README.md
CHANGED
|
@@ -3,19 +3,22 @@
|
|
|
3
3
|
OpenCode adapter for `gitwt`. It exposes the shared worktree core as agent tools:
|
|
4
4
|
https://github.com/voischev/gitwt
|
|
5
5
|
|
|
6
|
-
Requires Node.js 22.14+ and Git. Supports OpenCode 1.19.3
|
|
6
|
+
Requires Node.js 22.14+ and Git. Supports OpenCode V1 (1.19.3+) and V2 (2.2.5+).
|
|
7
7
|
|
|
8
8
|
## Use in OpenCode
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
For **OpenCode V2**, add to your project's `opencode.json`:
|
|
11
11
|
|
|
12
12
|
```json
|
|
13
13
|
{
|
|
14
14
|
"$schema": "https://opencode.ai/config.json",
|
|
15
|
-
"
|
|
15
|
+
"plugins": ["opencode-gitwt@latest"]
|
|
16
16
|
}
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
+
For **V1**, use `"plugin": ["opencode-gitwt@latest"]` instead. Restart OpenCode
|
|
20
|
+
after installation. Both versions expose the same four gitwt tools.
|
|
21
|
+
|
|
19
22
|
Ask the agent in chat, for example:
|
|
20
23
|
|
|
21
24
|
> Create a worktree on branch feature/login from main, fix the login issue there,
|
|
@@ -43,3 +46,23 @@ and `~/.config/gitwt/config.json` globally. Options include `remote`, `base`,
|
|
|
43
46
|
|
|
44
47
|
For the standalone `gitwt` command, run `npm install -g @voischev/gitwt`.
|
|
45
48
|
Move existing settings to these paths and remove comments and trailing commas.
|
|
49
|
+
|
|
50
|
+
## Why use it with OpenCode V2?
|
|
51
|
+
|
|
52
|
+
V2 already has native worktree tools, setup commands, and session moves.
|
|
53
|
+
Use gitwt when you want:
|
|
54
|
+
|
|
55
|
+
- **One workflow across agents:** the same standalone CLI, Node API, and JSON
|
|
56
|
+
contract in OpenCode, Codex, Claude Code, and scripts.
|
|
57
|
+
- **Named branches from a fresh remote base:** gitwt fetches the selected base
|
|
58
|
+
and verifies path, branch, and commit before setup. V2's built-in Git strategy
|
|
59
|
+
creates a detached checkout from a local ref or HEAD, without fetching.
|
|
60
|
+
- **Per-worktree lifecycle hooks:** saved `postCreate` and `preRemove` commands;
|
|
61
|
+
teardown failure blocks removal through gitwt, even with `--force`.
|
|
62
|
+
|
|
63
|
+
Prefer native V2 features for integrated UI navigation and actual session moves.
|
|
64
|
+
Native removal does not run gitwt's teardown hooks: remove gitwt-managed copies
|
|
65
|
+
through `gitwt_remove` or the CLI when those hooks matter.
|
|
66
|
+
|
|
67
|
+
Comparison verified against OpenCode V2.2.5's built-in Git strategy:
|
|
68
|
+
https://github.com/anomalyco/opencode/blob/v2.2.5/packages/core/src/worktree.ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-gitwt",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Git worktree tools for OpenCode, powered by gitwt",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
],
|
|
16
16
|
"engines": {
|
|
17
17
|
"node": ">=22.14.0",
|
|
18
|
-
"opencode": ">=1.19.3 <2"
|
|
18
|
+
"opencode": ">=1.19.3 <2 || >=2.2.5 <3"
|
|
19
19
|
},
|
|
20
20
|
"scripts": {
|
|
21
21
|
"check": "biome check .",
|
|
@@ -23,8 +23,7 @@
|
|
|
23
23
|
"release": "node scripts/release.js"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@voischev/gitwt": "^0.1.4"
|
|
27
|
-
"zod": "4.1.8"
|
|
26
|
+
"@voischev/gitwt": "^0.1.4"
|
|
28
27
|
},
|
|
29
28
|
"devDependencies": {
|
|
30
29
|
"@biomejs/biome": "2.6.3"
|
package/src/index.js
CHANGED
|
@@ -1,69 +1,130 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
createWorktree,
|
|
3
|
+
GitwtError,
|
|
4
|
+
listWorktrees,
|
|
5
|
+
removeWorktree,
|
|
6
|
+
resolveWorktree,
|
|
7
|
+
} from "@voischev/gitwt";
|
|
3
8
|
|
|
4
9
|
export async function GitwtPlugin({ directory }, config = {}) {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
.optional()
|
|
16
|
-
.describe("Local ref or verified commit instead of a remote base"),
|
|
17
|
-
path: z.string().optional().describe("Destination path"),
|
|
18
|
-
noHooks: z.boolean().optional().describe("Disable hooks for this worktree"),
|
|
10
|
+
const tool = {
|
|
11
|
+
gitwt_create: {
|
|
12
|
+
description:
|
|
13
|
+
"Create and verify a new worktree. Use worktreePath for commands and files in the current session. Keep the worktree when pausing or handing off a task.",
|
|
14
|
+
args: {
|
|
15
|
+
branch: { type: "string", description: "New branch name" },
|
|
16
|
+
base: { type: "string", description: "Remote base branch" },
|
|
17
|
+
from: {
|
|
18
|
+
type: "string",
|
|
19
|
+
description: "Local ref or verified commit instead of a remote base",
|
|
19
20
|
},
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
await createWorktree(ctx.directory ?? directory, {
|
|
23
|
-
...args,
|
|
24
|
-
signal: ctx.abort,
|
|
25
|
-
config: { ...config, base: args.base ?? config.base },
|
|
26
|
-
}),
|
|
27
|
-
),
|
|
28
|
-
},
|
|
29
|
-
gitwt_list: {
|
|
30
|
-
description:
|
|
31
|
-
"List Git worktrees. With status, check dirty and available ahead/behind counts; null means unknown.",
|
|
32
|
-
args: { status: z.boolean().optional().describe("Check live worktree status") },
|
|
33
|
-
execute: async (args, ctx) =>
|
|
34
|
-
JSON.stringify(await listWorktrees(ctx.directory ?? directory, args)),
|
|
35
|
-
},
|
|
36
|
-
gitwt_cd: {
|
|
37
|
-
description:
|
|
38
|
-
"Resolve a branch or full path to a worktree with HEAD and status. Use worktreePath explicitly; this does not change the session directory.",
|
|
39
|
-
args: { target: z.string().describe("Exact branch name or full worktree path") },
|
|
40
|
-
execute: async ({ target }, ctx) =>
|
|
41
|
-
JSON.stringify(
|
|
42
|
-
await resolveWorktree(ctx.directory ?? directory, target, { status: true }),
|
|
43
|
-
),
|
|
21
|
+
path: { type: "string", description: "Destination path" },
|
|
22
|
+
noHooks: { type: "boolean", description: "Disable hooks for this worktree" },
|
|
44
23
|
},
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
24
|
+
execute: async (args, ctx) =>
|
|
25
|
+
JSON.stringify(
|
|
26
|
+
await createWorktree(ctx.directory ?? directory, {
|
|
27
|
+
...args,
|
|
28
|
+
signal: ctx.abort,
|
|
29
|
+
config: { ...config, base: args.base ?? config.base },
|
|
30
|
+
}),
|
|
31
|
+
),
|
|
32
|
+
},
|
|
33
|
+
gitwt_list: {
|
|
34
|
+
description:
|
|
35
|
+
"List Git worktrees. With status, check dirty and available ahead/behind counts; null means unknown.",
|
|
36
|
+
args: { status: { type: "boolean", description: "Check live worktree status" } },
|
|
37
|
+
execute: async (args, ctx) =>
|
|
38
|
+
JSON.stringify(await listWorktrees(ctx.directory ?? directory, args)),
|
|
39
|
+
},
|
|
40
|
+
gitwt_cd: {
|
|
41
|
+
description:
|
|
42
|
+
"Resolve a branch or full path to a worktree with HEAD and status. Use worktreePath explicitly; this does not change the session directory.",
|
|
43
|
+
args: { target: { type: "string", description: "Exact branch name or full worktree path" } },
|
|
44
|
+
execute: async ({ target }, ctx) =>
|
|
45
|
+
JSON.stringify(await resolveWorktree(ctx.directory ?? directory, target, { status: true })),
|
|
46
|
+
},
|
|
47
|
+
gitwt_remove: {
|
|
48
|
+
description:
|
|
49
|
+
"Remove a worktree only when explicitly requested and no longer in use. Force discards local changes but does not bypass a failing preRemove hook.",
|
|
50
|
+
args: {
|
|
51
|
+
target: { type: "string", description: "Exact branch name or full worktree path" },
|
|
52
|
+
force: { type: "boolean", description: "Allow discarding local changes" },
|
|
53
|
+
keepBranch: { type: "boolean", description: "Keep the branch after removing the worktree" },
|
|
64
54
|
},
|
|
55
|
+
execute: async ({ target, force, keepBranch }, ctx) =>
|
|
56
|
+
JSON.stringify(
|
|
57
|
+
await removeWorktree(ctx.directory ?? directory, target, {
|
|
58
|
+
force,
|
|
59
|
+
deleteBranch: !keepBranch,
|
|
60
|
+
signal: ctx.abort,
|
|
61
|
+
}),
|
|
62
|
+
),
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
const required = {
|
|
66
|
+
gitwt_create: ["branch"],
|
|
67
|
+
gitwt_list: [],
|
|
68
|
+
gitwt_cd: ["target"],
|
|
69
|
+
gitwt_remove: ["target"],
|
|
70
|
+
};
|
|
71
|
+
for (const [id, definition] of Object.entries(tool)) {
|
|
72
|
+
const execute = definition.execute;
|
|
73
|
+
definition.execute = async (args, ctx) => {
|
|
74
|
+
if (!args || typeof args !== "object" || Array.isArray(args)) {
|
|
75
|
+
throw new GitwtError("invalid_arguments", `${id}: expected an argument object`);
|
|
76
|
+
}
|
|
77
|
+
for (const key of Object.keys(args)) {
|
|
78
|
+
if (!Object.hasOwn(definition.args, key)) {
|
|
79
|
+
throw new GitwtError("invalid_arguments", `${id}: unknown argument ${key}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
for (const [key, schema] of Object.entries(definition.args)) {
|
|
83
|
+
if (args[key] === undefined && !required[id].includes(key)) continue;
|
|
84
|
+
if (typeof args[key] !== schema.type) {
|
|
85
|
+
throw new GitwtError("invalid_arguments", `${id}: ${key} must be ${schema.type}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return execute(args, ctx);
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
tool,
|
|
93
|
+
"tool.definition": async ({ toolID }, output) => {
|
|
94
|
+
if (!Object.hasOwn(tool, toolID)) return;
|
|
95
|
+
output.jsonSchema = {
|
|
96
|
+
type: "object",
|
|
97
|
+
properties: tool[toolID].args,
|
|
98
|
+
required: required[toolID],
|
|
99
|
+
additionalProperties: false,
|
|
100
|
+
};
|
|
65
101
|
},
|
|
66
102
|
};
|
|
67
103
|
}
|
|
68
104
|
|
|
69
|
-
export default {
|
|
105
|
+
export default {
|
|
106
|
+
id: "opencode-gitwt",
|
|
107
|
+
server: GitwtPlugin,
|
|
108
|
+
async setup(ctx) {
|
|
109
|
+
const directory = ctx.location.directory;
|
|
110
|
+
const hooks = await GitwtPlugin({ directory }, ctx.options);
|
|
111
|
+
const definitions = [];
|
|
112
|
+
for (const [name, definition] of Object.entries(hooks.tool)) {
|
|
113
|
+
const output = {};
|
|
114
|
+
await hooks["tool.definition"]({ toolID: name }, output);
|
|
115
|
+
definitions.push({
|
|
116
|
+
name,
|
|
117
|
+
description: definition.description,
|
|
118
|
+
input: output.jsonSchema,
|
|
119
|
+
options: { codemode: false },
|
|
120
|
+
async execute(args, call) {
|
|
121
|
+
call.signal.throwIfAborted();
|
|
122
|
+
return { content: await definition.execute(args, { directory, abort: call.signal }) };
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
await ctx.tool.transform((editor) => {
|
|
127
|
+
for (const definition of definitions) editor.add(definition);
|
|
128
|
+
});
|
|
129
|
+
},
|
|
130
|
+
};
|