feat(trail): discussion, attachment, and watcher commands · Entire
feat(trail): discussion, attachment, and watcher commands
f36d3c2→main·
computermode·1w ago·5 files·+749 added/-0 removed
Add 'trail comment' (list/show/add/reply/edit/delete/resolve/unresolve), 'trail attachment' (list/add/remove; raw-binary image upload), and read-only 'trail watchers'. Completes UI parity for trail collaboration.
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com
Sessions
01KX6NQAZGY2WRCHB3458WC0ZBView transcript
Changes
5
cmd/entire/cli
Atrail_attachment_cmd.go+184
Mtrail_cmd.go+3
Atrail_comment_cmd.go+384
Atrail_phase2_cmd_test.go+97
Atrail_watchers_cmd.go+81
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
package cli
import ( "bytes" "context" "encoding/json" "errors" "fmt" "net/http" "net/url" "os" "path/filepath" "strings"
"github.com/entireio/cli/cmd/entire/cli/api" "github.com/spf13/cobra" )
// trailAttachmentsPath builds the attachments collection path for a trail number. func trailAttachmentsPath(forge, owner, repo string, number int) string { return trailNumberPath(forge, owner, repo, number) + "/attachments" }
func trailAttachmentPath(forge, owner, repo string, number int, id string) string { return trailAttachmentsPath(forge, owner, repo, number) + "/" + id }
// detectAttachmentContentType returns the image MIME type for a file's bytes, // accepting only PNG and JPEG (the only kinds the server stores). It errors on // anything else so the CLI fails fast with a clear message. func detectAttachmentContentType(data []byte) (string, error) { ct := http.DetectContentType(data) switch ct { case "image/png", "image/jpeg": return ct, nil default: return "", fmt.Errorf("unsupported attachment type %q: only PNG and JPEG images are supported", ct) } }
func newTrailAttachmentCmd() *cobra.Command { cmd := &cobra.Command{ Use: "attachment", Short: "Manage image attachments on a trail", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, _ []string) error { return cmd.Help() }, } cmd.PersistentFlags().String("trail", "", "Trail number, id, or branch (defaults to the current branch)") cmd.PersistentFlags().String("branch", "", "Branch of the trail (defaults to current); cannot be combined with --trail")
cmd.AddCommand(newTrailAttachmentListCmd()) cmd.AddCommand(newTrailAttachmentAddCmd()) cmd.AddCommand(newTrailAttachmentRemoveCmd()) return cmd }
func newTrailAttachmentListCmd() *cobra.Command { var jsonOut bool cmd := &cobra.Command{ Use: "list", Short: "List attachments on a trail", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, _ []string) error { return withNumberedTrail(cmd, cmd.OutOrStdout(), func(ctx context.Context, client *api.Client, found *api.TrailResource, forge, owner, repo string) error { resp, err := client.Get(ctx, trailAttachmentsPath(forge, owner, repo, found.Number)) if err != nil { return fmt.Errorf("failed to list attachments: %w", err) } defer resp.Body.Close() if err := checkTrailResponse(resp); err != nil { return err } var out api.TrailAttachmentsResponse if err := api.DecodeJSON(resp, &out); err != nil { return fmt.Errorf("failed to decode attachments response: %w", err) } if jsonOut { enc := json.NewEncoder(cmd.OutOrStdout()) enc.SetIndent("", " ") return enc.Encode(out) } if len(out.Attachments) == 0 { fmt.Fprintf(cmd.OutOrStdout(), "No attachments on trail #%d\n", found.Number) return nil } for _, a := range out.Attachments { fmt.Fprintf(cmd.OutOrStdout(), "%s %s %s %d bytes %s\n", a.ID, a.Filename, a.ContentType, a.SizeBytes, a.CreatedAt) } return nil }) }, } cmd.Flags().BoolVar(&jsonOut, "json", false, "Output as JSON") return cmd }
func newTrailAttachmentAddCmd() *cobra.Command {
var jsonOut bool
cmd := &cobra.Command{
Use: "add
// trailAttachmentURL builds the public inline-variant serve URL for an // attachment. The host/owner/repo segments are decorative server-side; the id // alone resolves the attachment. func trailAttachmentURL(forge, owner, repo, id string) string { return strings.TrimRight(api.BaseURL(), "/") + "/api/v1/trails/" + forge + "/" + owner + "/" + repo + "/attachments/" + id + "/inline" }
func newTrailAttachmentRemoveCmd() *cobra.Command {
var force bool
cmd := &cobra.Command{
Use: "remove