Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 34 additions & 9 deletions internal/commands/comment.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package commands
import (
"errors"
"fmt"
"io"
"os"
"strconv"
"strings"
Expand Down Expand Up @@ -198,7 +199,10 @@ func newCommentsUpdateCmd() *cobra.Command {

You can pass either a comment ID or a Basecamp URL:
basecamp comments update 789 "new text"
basecamp comments update https://3.basecamp.com/123/buckets/456/todos/111#__recording_789 "new text"`,
basecamp comments update https://3.basecamp.com/123/buckets/456/todos/111#__recording_789 "new text"

Use - as the content argument to read the updated content from stdin:
basecamp comments update 789 - < body.md`,
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
return missingArg(cmd, "<id|url>")
Expand All @@ -207,6 +211,11 @@ You can pass either a comment ID or a Basecamp URL:
return missingArg(cmd, "<content>")
}

content, err := contentArgOrStdin(cmd, args[1:])
if err != nil {
return err
}

app := appctx.FromContext(cmd.Context())
if err := ensureAccount(cmd, app); err != nil {
return err
Expand All @@ -216,8 +225,6 @@ You can pass either a comment ID or a Basecamp URL:
// Uses extractCommentWithProject to prefer CommentID from URL fragments
commentIDStr, _ := extractCommentWithProject(args[0])

content := strings.Join(args[1:], " ")

commentID, err := strconv.ParseInt(commentIDStr, 10, 64)
if err != nil {
return output.ErrUsage("Invalid comment ID")
Expand Down Expand Up @@ -299,7 +306,10 @@ Comma-separated IDs add the same comment to multiple items:
basecamp comment https://3.basecamp.com/123/buckets/456/todos/789 "Looks good!"

Content supports Markdown and @mentions (@Name or @First.Last):
basecamp comment 789 "Hey @Jane.Smith, **please review**"`,
basecamp comment 789 "Hey @Jane.Smith, **please review**"

Use - as the content argument to read content from stdin:
basecamp comment 789 - < body.md`,
Annotations: map[string]string{"agent_notes": "Comments are flat — reply to parent item, not to other comments\nURL fragments (#__recording_456) are comment IDs — comment on the parent recording_id, not the comment_id\nComments are on items (todos, messages, cards, etc.) — not on other comments"},
RunE: func(cmd *cobra.Command, args []string) error {
app := appctx.FromContext(cmd.Context())
Expand All @@ -312,13 +322,17 @@ Content supports Markdown and @mentions (@Name or @First.Last):
// First arg is always the recording ID(s)
recordingArg := args[0]

var content string
if len(args) > 1 {
content = strings.Join(args[1:], " ")
if edit && len(args) > 1 {
return output.ErrUsage("cannot combine --edit and positional content")
}

if edit && content != "" {
return output.ErrUsage("cannot combine --edit and positional content")
var content string
if len(args) > 1 {
var err error
content, err = contentArgOrStdin(cmd, args[1:])
if err != nil {
return err
}
Comment thread
robzolkos marked this conversation as resolved.
}
if edit {
fi, err := os.Stdin.Stat()
Expand Down Expand Up @@ -479,3 +493,14 @@ Content supports Markdown and @mentions (@Name or @First.Last):

return cmd
}

func contentArgOrStdin(cmd *cobra.Command, args []string) (string, error) {
if len(args) == 1 && args[0] == "-" {
b, err := io.ReadAll(cmd.InOrStdin())
if err != nil {
return "", output.ErrUsage(fmt.Sprintf("failed to read content from stdin: %v", err))
}
return string(b), nil
}
return strings.Join(args, " "), nil
}
82 changes: 82 additions & 0 deletions internal/commands/comment_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
package commands

import (
"bytes"
"encoding/json"
"io"
"net/http"
"strings"
"testing"

"github.com/basecamp/basecamp-sdk/go/pkg/basecamp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/basecamp/basecamp-cli/internal/appctx"
"github.com/basecamp/basecamp-cli/internal/names"
)

// TestCommentShortcutAcceptsInFlag tests that the top-level 'comment' shortcut
Expand Down Expand Up @@ -56,3 +65,76 @@ func TestCommentsGroupAcceptsInFlag(t *testing.T) {
assert.NotContains(t, err.Error(), "unknown flag")
assert.NotContains(t, err.Error(), "unknown shorthand")
}

func TestCommentsCreateReadsDashContentFromStdin(t *testing.T) {
transport := &mockCommentWriteTransport{}
app, _ := setupCommentsWriteTestApp(t, transport)

cmd := newCommentsCreateCmd()
cmd.SetIn(strings.NewReader("Hello from stdin\n\n**works**\n"))

err := executeCommand(cmd, app, "789", "-")
require.NoError(t, err)
require.Len(t, transport.capturedBodies, 1)

var body map[string]string
require.NoError(t, json.Unmarshal(transport.capturedBodies[0], &body))
assert.Contains(t, body["content"], "Hello from stdin")
assert.Contains(t, body["content"], "<strong>works</strong>")
assert.NotEqual(t, "<p>-</p>", body["content"])
}

func TestCommentsUpdateReadsDashContentFromStdin(t *testing.T) {
transport := &mockCommentWriteTransport{}
app, _ := setupCommentsWriteTestApp(t, transport)

cmd := NewCommentsCmd()
cmd.SetIn(strings.NewReader("Updated from stdin\n"))

err := executeCommand(cmd, app, "update", "1234", "-")
require.NoError(t, err)
require.Len(t, transport.capturedBodies, 1)

var body map[string]string
require.NoError(t, json.Unmarshal(transport.capturedBodies[0], &body))
assert.Equal(t, "<p>Updated from stdin</p>", body["content"])
}

type mockCommentWriteTransport struct {
capturedBodies [][]byte
}

func (t *mockCommentWriteTransport) RoundTrip(req *http.Request) (*http.Response, error) {
if req.Body != nil {
body, _ := io.ReadAll(req.Body)
_ = req.Body.Close()
t.capturedBodies = append(t.capturedBodies, body)
}

header := make(http.Header)
header.Set("Content-Type", "application/json")

status := http.StatusOK
if req.Method == http.MethodPost {
status = http.StatusCreated
}

return &http.Response{
StatusCode: status,
Body: io.NopCloser(strings.NewReader(`{"id":1234,"content":"ok","status":"active"}`)),
Header: header,
}, nil
}

func setupCommentsWriteTestApp(t *testing.T, transport http.RoundTripper) (*appctx.App, *bytes.Buffer) {
t.Helper()

app, buf := setupTestApp(t)
sdkClient := basecamp.NewClient(&basecamp.Config{}, &testTokenProvider{},
basecamp.WithTransport(transport),
basecamp.WithMaxRetries(1),
)
app.SDK = sdkClient
app.Names = names.NewResolver(sdkClient, app.Auth, app.Config.AccountID)
return app, buf
}
13 changes: 13 additions & 0 deletions internal/commands/edit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,19 @@ func TestEditContentMutualExclusion(t *testing.T) {
}
})

t.Run("comment --edit with dash content", func(t *testing.T) {
err := runCmdWithFlagsAndArgs(NewCommentCmd,
map[string]string{"edit": "true"},
[]string{"12345", "-"},
)
if err == nil {
t.Fatal("expected error for --edit + dash content, got nil")
}
if !strings.Contains(err.Error(), "cannot combine") {
t.Errorf("error = %q, want 'cannot combine' message", err)
}
})

t.Run("message --edit with positional body", func(t *testing.T) {
err := runCmdWithFlagsAndArgs(NewMessageCmd,
map[string]string{"edit": "true"},
Expand Down
Loading