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 "paths":{"type":"array","items":{"type":"string"},"description":"Exact registered worktree paths. Required for cleanup."}
26 },
27 "required":["action"]
28}`
29
30func (t *WorktreeTool) Name() string { return "worktree" }
31func (t *WorktreeTool) Description() string {
32 return "Create, list, and safely clean up Git worktrees."
33}
34func (t *WorktreeTool) Schema() json.RawMessage { return json.RawMessage(worktreeSchema) }
35
36func (t *WorktreeTool) Preview(ctx context.Context, raw json.RawMessage) (core.ToolResult, error) {
37 action, delegated, err := parseWorktreeAction(raw)
38 if err != nil {
39 return core.ToolResult{}, err
40 }
41 switch action {
42 case "create":
43 if t.Create == nil {
44 return core.ToolResult{}, errors.New("worktree: create action is unavailable")
45 }
46 result, err := t.Create.Preview(ctx, delegated)
47 return result, worktreeError(err)
48 case "list", "cleanup":
49 if t.Manage == nil {
50 return core.ToolResult{}, errors.New("worktree: management actions are unavailable")
51 }
52 result, err := t.Manage.Preview(ctx, delegated)
53 return result, worktreeError(err)
54 default:
55 return core.ToolResult{}, errors.New("worktree: unknown action")
56 }
57}
58
59func (t *WorktreeTool) Execute(ctx context.Context, raw json.RawMessage, progress func(string)) (core.ToolResult, error) {
60 action, delegated, err := parseWorktreeAction(raw)
61 if err != nil {
62 return core.ToolResult{}, err
63 }
64 switch action {
65 case "create":
66 if t.Create == nil {
67 return core.ToolResult{}, errors.New("worktree: create action is unavailable")
68 }
69 result, err := t.Create.Execute(ctx, delegated, progress)
70 return result, worktreeError(err)
71 case "list", "cleanup":
72 if t.Manage == nil {
73 return core.ToolResult{}, errors.New("worktree: management actions are unavailable")
74 }
75 result, err := t.Manage.Execute(ctx, delegated, progress)
76 return result, worktreeError(err)
77 default:
78 return core.ToolResult{}, errors.New("worktree: unknown action")
79 }
80}
81
82func parseWorktreeAction(raw json.RawMessage) (string, json.RawMessage, error) {
83 var values map[string]json.RawMessage
84 if err := json.Unmarshal(raw, &values); err != nil {
85 return "", nil, err
86 }
87 var action string
88 if err := json.Unmarshal(values["action"], &action); err != nil || action == "" {
89 return "", nil, errors.New("worktree: action is required")
90 }
91 delete(values, "action")
92 if action == "list" || action == "cleanup" {
93 encoded, err := json.Marshal(map[string]any{"action": action, "paths": rawField(values, "paths")})
94 if err != nil {
95 return "", nil, err
96 }
97 if action == "list" {
98 encoded = json.RawMessage(`{"action":"list"}`)
99 }
100 return action, encoded, nil
101 }
102 encoded, err := json.Marshal(map[string]any{"branch": rawField(values, "branch")})
103 return action, encoded, err
104}
105
106func worktreeError(err error) error {
107 if err == nil {
108 return nil
109 }
110 message := err.Error()
111 for _, prefix := range []string{"create_worktree: ", "manage_worktrees: "} {
112 if strings.HasPrefix(message, prefix) {
113 message = strings.TrimPrefix(message, prefix)
114 break
115 }
116 }
117 return fmt.Errorf("worktree: %s", message)
118}
119
120func rawField(values map[string]json.RawMessage, key string) any {
121 value, ok := values[key]
122 if !ok {
123 return nil
124 }
125 var decoded any
126 if json.Unmarshal(value, &decoded) != nil {
127 return nil
128 }
129 return decoded
130}