Skip to content
Merged
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
7 changes: 7 additions & 0 deletions .surface
Original file line number Diff line number Diff line change
Expand Up @@ -3503,6 +3503,7 @@ FLAG basecamp checkin question create --styled type=bool
FLAG basecamp checkin question create --time type=string
FLAG basecamp checkin question create --todolist type=string
FLAG basecamp checkin question create --verbose type=count
FLAG basecamp checkin question create --visible-to-clients type=bool
FLAG basecamp checkin question show --account type=string
FLAG basecamp checkin question show --agent type=bool
FLAG basecamp checkin question show --all-comments type=bool
Expand Down Expand Up @@ -3772,6 +3773,7 @@ FLAG basecamp checkins question create --styled type=bool
FLAG basecamp checkins question create --time type=string
FLAG basecamp checkins question create --todolist type=string
FLAG basecamp checkins question create --verbose type=count
FLAG basecamp checkins question create --visible-to-clients type=bool
FLAG basecamp checkins question show --account type=string
FLAG basecamp checkins question show --agent type=bool
FLAG basecamp checkins question show --all-comments type=bool
Expand Down Expand Up @@ -9567,6 +9569,7 @@ FLAG basecamp messages create --styled type=bool
FLAG basecamp messages create --subscribe type=string
FLAG basecamp messages create --todolist type=string
FLAG basecamp messages create --verbose type=count
FLAG basecamp messages create --visible-to-clients type=bool
FLAG basecamp messages list --account type=string
FLAG basecamp messages list --agent type=bool
FLAG basecamp messages list --all type=bool
Expand Down Expand Up @@ -9977,6 +9980,7 @@ FLAG basecamp msgs create --styled type=bool
FLAG basecamp msgs create --subscribe type=string
FLAG basecamp msgs create --todolist type=string
FLAG basecamp msgs create --verbose type=count
FLAG basecamp msgs create --visible-to-clients type=bool
FLAG basecamp msgs list --account type=string
FLAG basecamp msgs list --agent type=bool
FLAG basecamp msgs list --all type=bool
Expand Down Expand Up @@ -11205,6 +11209,7 @@ FLAG basecamp schedule create --summary type=string
FLAG basecamp schedule create --title type=string
FLAG basecamp schedule create --todolist type=string
FLAG basecamp schedule create --verbose type=count
FLAG basecamp schedule create --visible-to-clients type=bool
FLAG basecamp schedule entries --account type=string
FLAG basecamp schedule entries --agent type=bool
FLAG basecamp schedule entries --all type=bool
Expand Down Expand Up @@ -12426,6 +12431,7 @@ FLAG basecamp todolist create --styled type=bool
FLAG basecamp todolist create --todolist type=string
FLAG basecamp todolist create --todoset type=string
FLAG basecamp todolist create --verbose type=count
FLAG basecamp todolist create --visible-to-clients type=bool
FLAG basecamp todolist list --account type=string
FLAG basecamp todolist list --agent type=bool
FLAG basecamp todolist list --all type=bool
Expand Down Expand Up @@ -12968,6 +12974,7 @@ FLAG basecamp todolists create --styled type=bool
FLAG basecamp todolists create --todolist type=string
FLAG basecamp todolists create --todoset type=string
FLAG basecamp todolists create --verbose type=count
FLAG basecamp todolists create --visible-to-clients type=bool
FLAG basecamp todolists list --account type=string
FLAG basecamp todolists list --agent type=bool
FLAG basecamp todolists list --all type=bool
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ require (
charm.land/bubbles/v2 v2.1.1
charm.land/bubbletea/v2 v2.0.8
charm.land/lipgloss/v2 v2.0.5
github.com/basecamp/basecamp-sdk/go v0.8.0
github.com/basecamp/basecamp-sdk/go v0.8.1-0.20260724184307-e2c1abea4aea
github.com/basecamp/cli v0.2.1
github.com/charmbracelet/bubbles v1.0.0
github.com/charmbracelet/glamour v1.0.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ
github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/basecamp/basecamp-sdk/go v0.8.0 h1:ZxrNTyGTMcGocGE582ZMmpjyodvODD2+fZMjISokTYY=
github.com/basecamp/basecamp-sdk/go v0.8.0/go.mod h1:eX5mEKCdtxSfEL4P/n5AwOl21JVA/K+gRPic/Hd8W/Y=
github.com/basecamp/basecamp-sdk/go v0.8.1-0.20260724184307-e2c1abea4aea h1:dRwhvhnzbyXxitGbb3uGFaUIJHazztD9UgWC0XzM870=
github.com/basecamp/basecamp-sdk/go v0.8.1-0.20260724184307-e2c1abea4aea/go.mod h1:eX5mEKCdtxSfEL4P/n5AwOl21JVA/K+gRPic/Hd8W/Y=
github.com/basecamp/cli v0.2.1 h1:8GyehPVtsTXla0oOPu4QgXRjwwzJ99prlByvyi+0HRQ=
github.com/basecamp/cli v0.2.1/go.mod h1:p8tt/DatJ2LAzWO6N6tNfV8x3gF5T3IxDTo+U8FfWPo=
github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
Expand Down
10 changes: 10 additions & 0 deletions internal/commands/checkins.go
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,7 @@ func newCheckinsQuestionCreateCmd(project *string) *cobra.Command {
var frequency string
var timeOfDay string
var days string
var visibleToClients bool

cmd := &cobra.Command{
Use: "create <title>",
Expand Down Expand Up @@ -366,6 +367,14 @@ Days format: comma-separated (0=Sun, 1=Mon, 2=Tue, 3=Wed, 4=Thu, 5=Fri, 6=Sat)`,
},
}

// Set client visibility only when the flag was provided. Omitting it
// uses the server's default: team-only when posting as a team member,
// but a client-authenticated caller always creates client-visible
// records (an explicit false is overridden server-side).
if cmd.Flags().Changed("visible-to-clients") {
req.VisibleToClients = &visibleToClients
}

question, err := app.Account().Checkins().CreateQuestion(cmd.Context(), qID, req)
if err != nil {
return convertSDKError(err)
Expand Down Expand Up @@ -393,6 +402,7 @@ Days format: comma-separated (0=Sun, 1=Mon, 2=Tue, 3=Wed, 4=Thu, 5=Fri, 6=Sat)`,
cmd.Flags().StringVarP(&frequency, "frequency", "f", "", "Schedule frequency (default: every_day)")
cmd.Flags().StringVar(&timeOfDay, "time", "", "Time to ask (default: 5:00pm)")
cmd.Flags().StringVarP(&days, "days", "d", "", "Days to ask, comma-separated (default: 1,2,3,4,5)")
cmd.Flags().BoolVar(&visibleToClients, "visible-to-clients", false, "Make the question visible to clients on the project (omit for the server default; client-authenticated callers always post client-visible)")

return cmd
}
Expand Down
89 changes: 89 additions & 0 deletions internal/commands/checkins_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,95 @@ func (m *mockCheckinsAnswerCreateTransport) RoundTrip(req *http.Request) (*http.
}
}

// mockCheckinsQuestionCreateTransport resolves the questionnaire via the project
// dock and captures the POST body sent to create a question.
type mockCheckinsQuestionCreateTransport struct {
recordedBody map[string]any
}

func (m *mockCheckinsQuestionCreateTransport) RoundTrip(req *http.Request) (*http.Response, error) {
header := make(http.Header)
header.Set("Content-Type", "application/json")

switch {
case req.Method == "GET" && strings.Contains(req.URL.Path, "/projects.json"):
return &http.Response{
StatusCode: 200,
Body: io.NopCloser(strings.NewReader(`[{"id":123,"name":"Test Project"}]`)),
Header: header,
}, nil
case req.Method == "GET" && strings.Contains(req.URL.Path, "/projects/"):
return &http.Response{
StatusCode: 200,
Body: io.NopCloser(strings.NewReader(`{"id":123,"dock":[{"name":"questionnaire","id":555,"enabled":true}]}`)),
Header: header,
}, nil
case req.Method == "POST" && strings.Contains(req.URL.Path, "/questions.json"):
if req.Body != nil {
defer req.Body.Close()
}
body, err := io.ReadAll(req.Body)
if err != nil {
return nil, err
}
if err := json.Unmarshal(body, &m.recordedBody); err != nil {
return nil, err
}
return &http.Response{
StatusCode: 201,
Body: io.NopCloser(strings.NewReader(`{"id":789,"title":"How are you?","type":"Question"}`)),
Header: header,
}, nil
default:
return &http.Response{
StatusCode: 404,
Body: io.NopCloser(strings.NewReader(`{"error":"Not Found"}`)),
Header: header,
}, nil
}
}

func runCheckinsQuestionCreate(t *testing.T, args ...string) *mockCheckinsQuestionCreateTransport {
t.Helper()
transport := &mockCheckinsQuestionCreateTransport{}
app, _ := newTestAppWithTransport(t, transport)
app.Config.ProjectID = "123"

project := ""
cmd := newCheckinsQuestionCreateCmd(&project)

err := executeCommand(cmd, app, args...)
require.NoError(t, err)
require.NotNil(t, transport.recordedBody, "expected request body to be captured")
return transport
}

func TestCheckinsQuestionCreateHasVisibleToClientsFlag(t *testing.T) {
project := ""
cmd := newCheckinsQuestionCreateCmd(&project)

flag := cmd.Flags().Lookup("visible-to-clients")
require.NotNil(t, flag, "expected --visible-to-clients flag on check-in question create")
}

func TestCheckinsQuestionCreateDefaultOmitsVisibleToClients(t *testing.T) {
transport := runCheckinsQuestionCreate(t, "How are you?")
_, ok := transport.recordedBody["visible_to_clients"]
assert.False(t, ok, "expected visible_to_clients to be omitted when flag is not set")
}

func TestCheckinsQuestionCreateVisibleToClientsTrue(t *testing.T) {
transport := runCheckinsQuestionCreate(t, "How are you?", "--visible-to-clients")
assert.Equal(t, true, transport.recordedBody["visible_to_clients"])
}

func TestCheckinsQuestionCreateVisibleToClientsFalse(t *testing.T) {
transport := runCheckinsQuestionCreate(t, "How are you?", "--visible-to-clients=false")
val, ok := transport.recordedBody["visible_to_clients"]
require.True(t, ok, "expected visible_to_clients present for explicit --visible-to-clients=false")
assert.Equal(t, false, val)
}

func TestCheckinsAnswerCreateDefaultsDateToToday(t *testing.T) {
originalNow := checkinsNow
checkinsNow = func() time.Time {
Expand Down
10 changes: 10 additions & 0 deletions internal/commands/messages.go
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,7 @@ func newMessagesCreateCmd(project *string, messageBoard *string) *cobra.Command
var subscribe string
var noSubscribe bool
var attachFiles []string
var visibleToClients bool

cmd := &cobra.Command{
Use: "create <title> [body]",
Expand Down Expand Up @@ -384,6 +385,14 @@ func newMessagesCreateCmd(project *string, messageBoard *string) *cobra.Command
req.Status = "active"
}

// Set client visibility only when the flag was provided. Omitting it
// uses the server's default: team-only when posting as a team member,
// but a client-authenticated caller always creates client-visible
// records (an explicit false is overridden server-side).
if cmd.Flags().Changed("visible-to-clients") {
req.VisibleToClients = &visibleToClients
}

message, err := app.Account().Messages().Create(cmd.Context(), boardID, req)
if err != nil {
return convertSDKError(err)
Expand Down Expand Up @@ -417,6 +426,7 @@ func newMessagesCreateCmd(project *string, messageBoard *string) *cobra.Command
cmd.Flags().StringVar(&subscribe, "subscribe", "", "Subscribe specific people (comma-separated names, emails, IDs, or \"me\")")
cmd.Flags().BoolVar(&noSubscribe, "no-subscribe", false, "Don't subscribe anyone else (silent, no notifications)")
cmd.Flags().StringArrayVar(&attachFiles, "attach", nil, "Attach file (repeatable)")
cmd.Flags().BoolVar(&visibleToClients, "visible-to-clients", false, "Make the message visible to clients on the project (omit for the server default; client-authenticated callers always post client-visible)")

return cmd
}
Expand Down
66 changes: 66 additions & 0 deletions internal/commands/messages_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,72 @@ func TestMessagesCreateSubscribeEmptyIsError(t *testing.T) {
assert.Contains(t, e.Message, "at least one person")
}

// TestMessagesCreateHasVisibleToClientsFlag tests that messages create has the --visible-to-clients flag.
func TestMessagesCreateHasVisibleToClientsFlag(t *testing.T) {
cmd := NewMessagesCmd()
createCmd, _, err := cmd.Find([]string{"create"})
require.NoError(t, err)

flag := createCmd.Flags().Lookup("visible-to-clients")
require.NotNil(t, flag, "expected --visible-to-clients flag on messages create")
}

// TestMessagesCreateDefaultOmitsVisibleToClients verifies that without the flag,
// visible_to_clients is omitted so the server applies its own default.
func TestMessagesCreateDefaultOmitsVisibleToClients(t *testing.T) {
transport := &mockMessageCreateTransport{}
app, _ := setupMessagesMockApp(t, transport)

cmd := NewMessagesCmd()

err := executeMessagesCommand(cmd, app, "create", "Normal post")
require.NoError(t, err)
require.NotEmpty(t, transport.capturedBody)

var body map[string]any
require.NoError(t, json.Unmarshal(transport.capturedBody, &body))

_, ok := body["visible_to_clients"]
assert.False(t, ok, "expected visible_to_clients to be omitted when flag is not set")
}

// TestMessagesCreateVisibleToClientsTrue verifies --visible-to-clients sends true.
func TestMessagesCreateVisibleToClientsTrue(t *testing.T) {
transport := &mockMessageCreateTransport{}
app, _ := setupMessagesMockApp(t, transport)

cmd := NewMessagesCmd()

err := executeMessagesCommand(cmd, app, "create", "Client post", "--visible-to-clients")
require.NoError(t, err)
require.NotEmpty(t, transport.capturedBody)

var body map[string]any
require.NoError(t, json.Unmarshal(transport.capturedBody, &body))

assert.Equal(t, true, body["visible_to_clients"])
}

// TestMessagesCreateVisibleToClientsFalse verifies --visible-to-clients=false
// sends an explicit false rather than dropping the field.
func TestMessagesCreateVisibleToClientsFalse(t *testing.T) {
transport := &mockMessageCreateTransport{}
app, _ := setupMessagesMockApp(t, transport)

cmd := NewMessagesCmd()

err := executeMessagesCommand(cmd, app, "create", "Team post", "--visible-to-clients=false")
require.NoError(t, err)
require.NotEmpty(t, transport.capturedBody)

var body map[string]any
require.NoError(t, json.Unmarshal(transport.capturedBody, &body))

val, ok := body["visible_to_clients"]
require.True(t, ok, "expected visible_to_clients present for explicit --visible-to-clients=false")
assert.Equal(t, false, val)
}

// mockMessageUpdateTransport handles PUT requests and captures the body.
type mockMessageUpdateTransport struct {
capturedBody []byte
Expand Down
14 changes: 12 additions & 2 deletions internal/commands/schedule.go
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,7 @@ func newScheduleCreateCmd(project, scheduleID *string) *cobra.Command {
var subscribe string
var noSubscribe bool
var attachFiles []string
var visibleToClients bool

cmd := &cobra.Command{
Use: "create <summary>",
Expand Down Expand Up @@ -438,7 +439,7 @@ func newScheduleCreateCmd(project, scheduleID *string) *cobra.Command {
return output.ErrUsage("--ends-at required (ISO 8601 datetime)")
}

return runScheduleCreate(cmd, app, *project, *scheduleID, entrySummary, startsAt, endsAt, description, allDay, notify, participants, subscribe, noSubscribe, attachFiles)
return runScheduleCreate(cmd, app, *project, *scheduleID, entrySummary, startsAt, endsAt, description, allDay, notify, visibleToClients, participants, subscribe, noSubscribe, attachFiles)
},
}

Expand All @@ -457,11 +458,12 @@ func newScheduleCreateCmd(project, scheduleID *string) *cobra.Command {
cmd.Flags().StringVar(&subscribe, "subscribe", "", "Subscribe specific people (comma-separated names, emails, IDs, or \"me\")")
cmd.Flags().BoolVar(&noSubscribe, "no-subscribe", false, "Don't subscribe anyone else (silent, no notifications)")
cmd.Flags().StringArrayVar(&attachFiles, "attach", nil, "Attach file (repeatable)")
cmd.Flags().BoolVar(&visibleToClients, "visible-to-clients", false, "Make the schedule entry visible to clients on the project (omit for the server default; client-authenticated callers always post client-visible)")

return cmd
}

func runScheduleCreate(cmd *cobra.Command, app *appctx.App, project, scheduleID, summary, startsAt, endsAt, description string, allDay, notify bool, participants, subscribe string, noSubscribe bool, attachFiles []string) error {
func runScheduleCreate(cmd *cobra.Command, app *appctx.App, project, scheduleID, summary, startsAt, endsAt, description string, allDay, notify, visibleToClients bool, participants, subscribe string, noSubscribe bool, attachFiles []string) error {
// Resolve subscription flags early (fail fast on bad input)
subs, err := applySubscribeFlags(cmd.Context(), app.Names, subscribe, cmd.Flags().Changed("subscribe"), noSubscribe)
if err != nil {
Expand Down Expand Up @@ -534,6 +536,14 @@ func runScheduleCreate(cmd *cobra.Command, app *appctx.App, project, scheduleID,
Subscriptions: subs,
}

// Set client visibility only when the flag was provided. Omitting it uses the
// server's default: team-only when posting as a team member, but a
// client-authenticated caller always creates client-visible records (an
// explicit false is overridden server-side).
if cmd.Flags().Changed("visible-to-clients") {
req.VisibleToClients = &visibleToClients
}
Comment thread
jeremy marked this conversation as resolved.

if participants != "" {
var ids []int64
for idStr := range strings.SplitSeq(participants, ",") {
Expand Down
Loading
Loading