# feat(trail): drop attachment and watcher commands for now

`3fced2a`→[main](/content/gh/entireio/cli/commits/main/index.html)·

computermode·1w ago·8 files·+1 added/-383 removed

Scope this PR to metadata, approvals, and discussion threads. Attachment
(raw-binary upload) and watchers (presence-based) support is removed for a
later change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

## Sessions

01KX6VR2GWJFA44C0255JA7E57View transcript

[?\
CLI Trail Commands and UI ParityClaude Code·Opus 4.8·2 steps](/content/gh/entireio/cli/session/a7c8f6a2-7a40-4558-aba8-a31927cc21d3#timeline-01KX6VR2GWJFA44C0255JA7E57/index.html)

## Changes

8

- cmd/entire/cli

- api

- Dtrail_attachment_types.go-29

- Mtrail_thread_types_test.go-18

- Dtrail_watcher_types.go-8

- Dtrail_attachment_cmd.go-184

- Mtrail_cmd.go-2

- Mtrail_collaboration_cmd_test.go-58

- Mtrail_comment_cmd.go+1/-2

```go
package api

// TrailAttachment is a trail attachment (v1: images only). The server strips
// the internal r2_key; there is no URL field — a serve URL is built from the
// id and a variant. Width/Height/CreatedByUserID/DeletedAt are nullable.
type TrailAttachment struct {
	ID              string  `json:"id"`
	TrailID         string  `json:"trail_id"`
	RepoID          string  `json:"repo_id"`
	Kind            string  `json:"kind"`
	ContentType     string  `json:"content_type"`
	Filename        string  `json:"filename"`
	SizeBytes       int64   `json:"size_bytes"`
	Width           *int    `json:"width"`
	Height          *int    `json:"height"`
	CreatedByUserID *string `json:"created_by_user_id"`
	CreatedAt       string  `json:"created_at"`
	DeletedAt       *string `json:"deleted_at"`
}

// TrailAttachmentsResponse is the response from GET .../:number/attachments.
type TrailAttachmentsResponse struct {
	Attachments []TrailAttachment `json:"attachments"`
}

// TrailAttachmentUploadResponse is the response from POST .../:number/attachments.
type TrailAttachmentUploadResponse struct {
	Attachment TrailAttachment `json:"attachment"`
}
```

Dcmd/entire/cli/api/trail_attachment_types.go-29

```go
package api

// TrailWatchersResponse is the response from GET /api/v1/trails/:trail_id/watchers.
// Watchers are the user IDs of clients currently connected to the trail's live
// event stream (presence-based, not a persisted subscription list).
type TrailWatchersResponse struct {
	Watchers []string `json:"watchers"`
}
```

Dcmd/entire/cli/api/trail_watcher_types.go-8

```go
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"
}

// 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)
	}
}

// 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"
}
```

Dcmd/entire/cli/trail_attachment_cmd.go-184

```go
package cli

import (
	"context"
	"errors"
	"fmt"
	"strings"

"github.com/entireio/cli/cmd/entire/cli/api"
	"github.com/spf13/cobra"
)

// withNumberedTrail resolves a numbered trail (by --trail selector or current
// branch / --branch) and invokes fn inside an authenticated API context. It
// centralizes the resolution boilerplate shared by the comment and attachment
// subtrees.
func withNumberedTrail(cmd *cobra.Command, fn func(ctx context.Context, client *api.Client, found *api.TrailResource, forge, owner, repo string) error) error {
	...
}
```

Dcmd/entire/cli/trail_cmd.go-2

```go
package cli

// trailWatchersPath returns the watchers path, keyed by the trail's UUID (not
// its number, unlike the other trail subresources).
func trailWatchersPath(trailID string) string {
	return "/api/v1/trails/" + trailID + "/watchers"
}
```

Dcmd/entire/cli/trail_watchers_cmd.go-82

```go
package cli

import (
	"context"
	"errors"
	"fmt"
	"strings"

"github.com/entireio/cli/cmd/entire/cli/api"
	"github.com/spf13/cobra"
)

func newTrailWatchersCmd() *cobra.Command {
	...
}
```
