1package tools
2
3import (
4 "context"
5 "encoding/json"
6 "errors"
7 "fmt"
8 "strings"
9
10 "github.com/bnema/zut/packages/core"
11)
12
13// WorktreeTool exposes persistent worktree creation and lifecycle management
14// through one public tool while keeping the implementations focused internally.
15type WorktreeTool struct {
16 Create *CreateWorktreeTool
17 Manage *ManageWorktreesTool
18}
19
20const worktreeSchema = `{
21 "type":"object",
22 "properties":{
23 "action":{"type":"string","enum":["create","list","cleanup"],"description":"Create a branch worktree, list repository worktrees, or remove explicitly selected safe worktrees."},
24 "branch":{"type":"string","description":"New branch name. Required for create."},
25 "base":{"type":"string","description":"create only: branch, tag, or commit the new branch starts from. Defaults to the current HEAD. For a sub-worktree of a feature branch (for example feat/x-api from feat/x), set base to that feature branch."},
26 "paths":{"type":"array","items":{"type":"string"},"description":"Exact registered worktree paths. Required for cleanup."},
27 "into":{"type":"string","description":"cleanup/list: branch, tag, or commit the worktree branches must be merged into to be removable. Defaults to the current HEAD. After merging sub-branches into a feature branch, set into to that feature branch."},
28 "delete_branch":{"type":"boolean","description":"cleanup only: also delete each removed worktree's local branch, only if it is still at the inspected commit. Never deletes the merge target or a branch checked out elsewhere."}
29 },
30 "required":["action"]
31}`
32
33func (t *WorktreeTool) Name() string { return "worktree" }
34func (t *WorktreeTool) Description() string {
35 return "Create, list, and safely clean up Git worktrees. Supports sub-worktrees based on a feature branch and cleanup of branches merged into it."
36}
37func (t *WorktreeTool) Schema() json.RawMessage { return json.RawMessage(worktreeSchema) }
38
39func (t *WorktreeTool) Preview(ctx context.Context, raw json.RawMessage) (core.ToolResult, error) {
40 action, delegated, err := parseWorktreeAction(raw)
41 if err != nil {
42 return core.ToolResult{}, err
43 }
44 switch action {
45 case "create":
46 if t.Create == nil {
47 return core.ToolResult{}, errors.New("worktree: create action is unavailable")
48 }
49 result, err := t.Create.Preview(ctx, delegated)
50 return result, worktreeError(err)
51 case "list", "cleanup":
52 if t.Manage == nil {
53 return core.ToolResult{}, errors.New("worktree: management actions are unavailable")
54 }
55 result, err := t.Manage.Preview(ctx, delegated)
56 return result, worktreeError(err)
57 default:
58 return core.ToolResult{}, errors.New("worktree: unknown action")
59 }
60}
61
62func (t *WorktreeTool) Execute(ctx context.Context, raw json.RawMessage, progress func(string)) (core.ToolResult, error) {
63 action, delegated, err := parseWorktreeAction(raw)
64 if err != nil {
65 return core.ToolResult{}, err
66 }
67 switch action {
68 case "create":
69 if t.Create == nil {
70 return core.ToolResult{}, errors.New("worktree: create action is unavailable")
71 }
72 result, err := t.Create.Execute(ctx, delegated, progress)
73 return result, worktreeError(err)
74 case "list", "cleanup":
75 if t.Manage == nil {
76 return core.ToolResult{}, errors.New("worktree: management actions are unavailable")
77 }
78 result, err := t.Manage.Execute(ctx, delegated, progress)
79 return result, worktreeError(err)
80 default:
81 return core.ToolResult{}, errors.New("worktree: unknown action")
82 }
83}
84
85func parseWorktreeAction(raw json.RawMessage) (string, json.RawMessage, error) {
86 var values map[string]json.RawMessage
87 if err := json.Unmarshal(raw, &values); err != nil {
88 return "", nil, err
89 }
90 var action string
91 if err := json.Unmarshal(values["action"], &action); err != nil || action == "" {
92 return "", nil, errors.New("worktree: action is required")
93 }
94 delete(values, "action")
95 var fields map[string]any
96 switch action {
97 case "list":
98 fields = map[string]any{"action": action, "into": rawField(values, "into")}
99 case "cleanup":
100 fields = map[string]any{
101 "action": action,
102 "paths": rawField(values, "paths"),
103 "into": rawField(values, "into"),
104 "delete_branch": rawField(values, "delete_branch"),
105 }
106 default:
107 fields = map[string]any{"branch": rawField(values, "branch"), "base": rawField(values, "base")}
108 }
109 encoded, err := json.Marshal(fields)
110 return action, encoded, err
111}
112
113func worktreeError(err error) error {
114 if err == nil {
115 return nil
116 }
117 message := err.Error()
118 for _, prefix := range []string{"create_worktree: ", "manage_worktrees: "} {
119 if strings.HasPrefix(message, prefix) {
120 message = strings.TrimPrefix(message, prefix)
121 break
122 }
123 }
124 return fmt.Errorf("worktree: %s", message)
125}
126
127func rawField(values map[string]json.RawMessage, key string) any {
128 value, ok := values[key]
129 if !ok {
130 return nil
131 }
132 var decoded any
133 if json.Unmarshal(value, &decoded) != nil {
134 return nil
135 }
136 return decoded
137}