diff --git a/internal/commands/boards.go b/internal/commands/boards.go index 273d19b..a502b76 100644 --- a/internal/commands/boards.go +++ b/internal/commands/boards.go @@ -39,7 +39,7 @@ type BoardOutput struct { } func (c *BoardsCmd) Run(args []string) error { - projectID, _, err := getProjectID(args) + projectID, _, err := getProjectID(args, 0) if err != nil { return err } diff --git a/internal/commands/campfires.go b/internal/commands/campfires.go index 655800c..b64a094 100644 --- a/internal/commands/campfires.go +++ b/internal/commands/campfires.go @@ -62,7 +62,7 @@ type CampfireLineBrief struct { } func (c *CampfireCmd) Run(args []string) error { - projectID, _, err := getProjectID(args) + projectID, _, err := getProjectID(args, 0) if err != nil { return err } @@ -116,7 +116,7 @@ type CampfirePostOutput struct { } func (c *CampfirePostCmd) Run(args []string) error { - projectID, remaining, err := getProjectID(args) + projectID, remaining, err := getProjectID(args, 0) if err != nil { return err } diff --git a/internal/commands/card.go b/internal/commands/card.go index d32c0f5..c7ffeb7 100644 --- a/internal/commands/card.go +++ b/internal/commands/card.go @@ -59,7 +59,7 @@ type CardDetailOutput struct { } func (c *CardCmd) Run(args []string) error { - projectID, remaining, err := getProjectID(args) + projectID, remaining, err := getProjectID(args, 1) if err != nil { return err } diff --git a/internal/commands/cards.go b/internal/commands/cards.go index e153df8..805ab01 100644 --- a/internal/commands/cards.go +++ b/internal/commands/cards.go @@ -56,7 +56,7 @@ type CardsOutput struct { } func (c *CardsCmd) Run(args []string) error { - projectID, remaining, err := getProjectID(args) + projectID, remaining, err := getProjectID(args, 1) if err != nil { return err } @@ -157,7 +157,7 @@ type ColumnsOutput struct { } func (c *ColumnsCmd) Run(args []string) error { - projectID, remaining, err := getProjectID(args) + projectID, remaining, err := getProjectID(args, 1) if err != nil { return err } @@ -212,7 +212,7 @@ type CardCreateOutput struct { } func (c *CardCreateCmd) Run(args []string) error { - projectID, remaining, err := getProjectID(args) + projectID, remaining, err := getProjectID(args, 1) if err != nil { return err } @@ -305,7 +305,7 @@ type CardUpdateOutput struct { } func (c *CardUpdateCmd) Run(args []string) error { - projectID, remaining, err := getProjectID(args) + projectID, remaining, err := getProjectID(args, 1) if err != nil { return err } diff --git a/internal/commands/comments.go b/internal/commands/comments.go index aa5febe..92ff078 100644 --- a/internal/commands/comments.go +++ b/internal/commands/comments.go @@ -18,7 +18,7 @@ type CommentAddOutput struct { } func (c *CommentAddCmd) Run(args []string) error { - projectID, remaining, err := getProjectID(args) + projectID, remaining, err := getProjectID(args, 1) if err != nil { return err } diff --git a/internal/commands/documents.go b/internal/commands/documents.go index f2abcfc..c9b7c88 100644 --- a/internal/commands/documents.go +++ b/internal/commands/documents.go @@ -68,7 +68,7 @@ type DocOutputBrief struct { } func (c *DocsCmd) Run(args []string) error { - projectID, _, err := getProjectID(args) + projectID, _, err := getProjectID(args, 0) if err != nil { return err } @@ -127,7 +127,7 @@ type DocDetailOutput struct { } func (c *DocCmd) Run(args []string) error { - projectID, remaining, err := getProjectID(args) + projectID, remaining, err := getProjectID(args, 1) if err != nil { return err } @@ -196,7 +196,7 @@ type DocCreateOutput struct { } func (c *DocCreateCmd) Run(args []string) error { - projectID, remaining, err := getProjectID(args) + projectID, remaining, err := getProjectID(args, 0) if err != nil { return err } diff --git a/internal/commands/events.go b/internal/commands/events.go index 1ea6419..02a09eb 100644 --- a/internal/commands/events.go +++ b/internal/commands/events.go @@ -95,7 +95,7 @@ type EventsProjectOutput struct { } func (c *EventsProjectCmd) Run(args []string) error { - projectID, _, err := getProjectID(args) + projectID, _, err := getProjectID(args, 0) if err != nil { return err } @@ -152,7 +152,7 @@ type EventsRecordingOutput struct { } func (c *EventsRecordingCmd) Run(args []string) error { - projectID, remaining, err := getProjectID(args) + projectID, remaining, err := getProjectID(args, 1) if err != nil { return err } diff --git a/internal/commands/getprojectid_test.go b/internal/commands/getprojectid_test.go new file mode 100644 index 0000000..9fd7391 --- /dev/null +++ b/internal/commands/getprojectid_test.go @@ -0,0 +1,172 @@ +package commands + +import ( + "os" + "path/filepath" + "testing" +) + +func TestGetProjectIDWithConfig(t *testing.T) { + // Create temp dir with .basecamp.yml + tmpDir := t.TempDir() + os.WriteFile(filepath.Join(tmpDir, ".basecamp.yml"), []byte("project_id: 28395586\n"), 0644) + + origDir, _ := os.Getwd() + defer os.Chdir(origDir) + os.Chdir(tmpDir) + + tests := []struct { + name string + args []string + minPositionalArgs int + wantProjectID string + wantRemaining []string + }{ + { + name: "no args, minPositional=0", + args: []string{}, + minPositionalArgs: 0, + wantProjectID: "28395586", + wantRemaining: []string{}, + }, + { + name: "board_id only, minPositional=1", + args: []string{"5437262823"}, + minPositionalArgs: 1, + wantProjectID: "28395586", + wantRemaining: []string{"5437262823"}, + }, + { + name: "board_id with flags, minPositional=1", + args: []string{"5437262823", "--column", "Triage"}, + minPositionalArgs: 1, + wantProjectID: "28395586", + wantRemaining: []string{"5437262823", "--column", "Triage"}, + }, + { + name: "two positional args, minPositional=2", + args: []string{"111", "222", "--to", "Done"}, + minPositionalArgs: 2, + wantProjectID: "28395586", + wantRemaining: []string{"111", "222", "--to", "Done"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + projectID, remaining, err := getProjectID(tt.args, tt.minPositionalArgs) + if err != nil { + t.Errorf("getProjectID() unexpected error: %v", err) + return + } + if projectID != tt.wantProjectID { + t.Errorf("getProjectID() projectID = %v, want %v", projectID, tt.wantProjectID) + } + if len(remaining) != len(tt.wantRemaining) { + t.Errorf("getProjectID() remaining = %v, want %v", remaining, tt.wantRemaining) + } + }) + } +} + +func TestGetProjectIDWithoutConfig(t *testing.T) { + // Use a temp dir with NO .basecamp.yml + tmpDir := t.TempDir() + + origDir, _ := os.Getwd() + defer os.Chdir(origDir) + os.Chdir(tmpDir) + + tests := []struct { + name string + args []string + minPositionalArgs int + wantProjectID string + wantRemaining []string + wantErr bool + }{ + { + name: "project_id and board_id, minPositional=1", + args: []string{"28395586", "5437262823"}, + minPositionalArgs: 1, + wantProjectID: "28395586", + wantRemaining: []string{"5437262823"}, + }, + { + name: "project_id, board_id, and flags, minPositional=1", + args: []string{"28395586", "5437262823", "--column", "Triage"}, + minPositionalArgs: 1, + wantProjectID: "28395586", + wantRemaining: []string{"5437262823", "--column", "Triage"}, + }, + { + name: "project_id only, minPositional=0", + args: []string{"28395586"}, + minPositionalArgs: 0, + wantProjectID: "28395586", + wantRemaining: []string{}, + }, + { + name: "three positional args, minPositional=2", + args: []string{"28395586", "111", "222", "--to", "Done"}, + minPositionalArgs: 2, + wantProjectID: "28395586", + wantRemaining: []string{"111", "222", "--to", "Done"}, + }, + // Error cases: not enough positional args + { + name: "no args, minPositional=0", + args: []string{}, + minPositionalArgs: 0, + wantErr: true, + }, + { + name: "no args, minPositional=1", + args: []string{}, + minPositionalArgs: 1, + wantErr: true, + }, + { + name: "only board_id, no project_id, minPositional=1", + args: []string{"5437262823"}, + minPositionalArgs: 1, + wantErr: true, + }, + { + name: "board_id with flags but no project_id, minPositional=1", + args: []string{"5437262823", "--column", "TRIAGE"}, + minPositionalArgs: 1, + wantErr: true, + }, + { + name: "only flags no positional args, minPositional=0", + args: []string{"--subject", "Hello", "--content", "World"}, + minPositionalArgs: 0, + wantErr: true, + }, + { + name: "one positional with flags, minPositional=2", + args: []string{"111", "--to", "Done"}, + minPositionalArgs: 2, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + projectID, remaining, err := getProjectID(tt.args, tt.minPositionalArgs) + if (err != nil) != tt.wantErr { + t.Errorf("getProjectID() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.wantErr { + if projectID != tt.wantProjectID { + t.Errorf("getProjectID() projectID = %v, want %v", projectID, tt.wantProjectID) + } + if len(remaining) != len(tt.wantRemaining) { + t.Errorf("getProjectID() remaining = %v, want %v", remaining, tt.wantRemaining) + } + } + }) + } +} diff --git a/internal/commands/message_types.go b/internal/commands/message_types.go index 380513f..0e1a5ea 100644 --- a/internal/commands/message_types.go +++ b/internal/commands/message_types.go @@ -32,7 +32,7 @@ type MessageTypeBrief struct { } func (c *MessageTypesCmd) Run(args []string) error { - projectID, _, err := getProjectID(args) + projectID, _, err := getProjectID(args, 0) if err != nil { return err } @@ -83,7 +83,7 @@ type MessageTypeDetailOutput struct { } func (c *MessageTypeCmd) Run(args []string) error { - projectID, remaining, err := getProjectID(args) + projectID, remaining, err := getProjectID(args, 1) if err != nil { return err } @@ -129,7 +129,7 @@ type MessageTypeCreateOutput struct { } func (c *MessageTypeCreateCmd) Run(args []string) error { - projectID, remaining, err := getProjectID(args) + projectID, remaining, err := getProjectID(args, 0) if err != nil { return err } @@ -200,7 +200,7 @@ type MessageTypeUpdateOutput struct { } func (c *MessageTypeUpdateCmd) Run(args []string) error { - projectID, remaining, err := getProjectID(args) + projectID, remaining, err := getProjectID(args, 1) if err != nil { return err } @@ -274,7 +274,7 @@ type MessageTypeDeleteOutput struct { } func (c *MessageTypeDeleteCmd) Run(args []string) error { - projectID, remaining, err := getProjectID(args) + projectID, remaining, err := getProjectID(args, 1) if err != nil { return err } diff --git a/internal/commands/messages.go b/internal/commands/messages.go index 2c809a7..ab070c3 100644 --- a/internal/commands/messages.go +++ b/internal/commands/messages.go @@ -68,7 +68,7 @@ type MessageOutputBrief struct { } func (c *MessagesCmd) Run(args []string) error { - projectID, _, err := getProjectID(args) + projectID, _, err := getProjectID(args, 0) if err != nil { return err } @@ -128,7 +128,7 @@ type MessageDetailOutput struct { } func (c *MessageCmd) Run(args []string) error { - projectID, remaining, err := getProjectID(args) + projectID, remaining, err := getProjectID(args, 1) if err != nil { return err } @@ -197,7 +197,7 @@ type MessageCreateOutput struct { } func (c *MessageCreateCmd) Run(args []string) error { - projectID, remaining, err := getProjectID(args) + projectID, remaining, err := getProjectID(args, 0) if err != nil { return err } diff --git a/internal/commands/move.go b/internal/commands/move.go index a5585f7..e254dc3 100644 --- a/internal/commands/move.go +++ b/internal/commands/move.go @@ -22,7 +22,7 @@ type CardTableForMove struct { } func (c *MoveCmd) Run(args []string) error { - projectID, remaining, err := getProjectID(args) + projectID, remaining, err := getProjectID(args, 2) if err != nil { return err } diff --git a/internal/commands/people.go b/internal/commands/people.go index e267b36..2836287 100644 --- a/internal/commands/people.go +++ b/internal/commands/people.go @@ -180,7 +180,7 @@ type PeopleProjectOutput struct { } func (c *PeopleProjectCmd) Run(args []string) error { - projectID, _, err := getProjectID(args) + projectID, _, err := getProjectID(args, 0) if err != nil { return err } @@ -264,7 +264,7 @@ type ProjectAccessOutput struct { } func (c *ProjectAccessCmd) Run(args []string) error { - projectID, remaining, err := getProjectID(args) + projectID, remaining, err := getProjectID(args, 0) if err != nil { return err } diff --git a/internal/commands/questionnaires.go b/internal/commands/questionnaires.go index e5ed421..14ef680 100644 --- a/internal/commands/questionnaires.go +++ b/internal/commands/questionnaires.go @@ -79,7 +79,7 @@ type QuestionnaireOutput struct { } func (c *QuestionnaireCmd) Run(args []string) error { - projectID, _, err := getProjectID(args) + projectID, _, err := getProjectID(args, 0) if err != nil { return err } @@ -119,7 +119,7 @@ type QuestionBrief struct { } func (c *QuestionsCmd) Run(args []string) error { - projectID, _, err := getProjectID(args) + projectID, _, err := getProjectID(args, 0) if err != nil { return err } @@ -179,7 +179,7 @@ type QuestionDetailOutput struct { } func (c *QuestionCmd) Run(args []string) error { - projectID, remaining, err := getProjectID(args) + projectID, remaining, err := getProjectID(args, 1) if err != nil { return err } @@ -255,7 +255,7 @@ type QuestionAnswerBrief struct { } func (c *QuestionAnswersCmd) Run(args []string) error { - projectID, remaining, err := getProjectID(args) + projectID, remaining, err := getProjectID(args, 1) if err != nil { return err } @@ -328,7 +328,7 @@ type QuestionAnswerDetailOutput struct { } func (c *QuestionAnswerCmd) Run(args []string) error { - projectID, remaining, err := getProjectID(args) + projectID, remaining, err := getProjectID(args, 1) if err != nil { return err } diff --git a/internal/commands/recordings.go b/internal/commands/recordings.go index ed21403..966f7d3 100644 --- a/internal/commands/recordings.go +++ b/internal/commands/recordings.go @@ -16,7 +16,7 @@ type RecordingStatusOutput struct { } func (c *ArchiveCmd) Run(args []string) error { - projectID, remaining, err := getProjectID(args) + projectID, remaining, err := getProjectID(args, 1) if err != nil { return err } @@ -47,7 +47,7 @@ func (c *ArchiveCmd) Run(args []string) error { type UnarchiveCmd struct{} func (c *UnarchiveCmd) Run(args []string) error { - projectID, remaining, err := getProjectID(args) + projectID, remaining, err := getProjectID(args, 1) if err != nil { return err } @@ -78,7 +78,7 @@ func (c *UnarchiveCmd) Run(args []string) error { type TrashCmd struct{} func (c *TrashCmd) Run(args []string) error { - projectID, remaining, err := getProjectID(args) + projectID, remaining, err := getProjectID(args, 1) if err != nil { return err } diff --git a/internal/commands/root.go b/internal/commands/root.go index e2e10be..03485e1 100644 --- a/internal/commands/root.go +++ b/internal/commands/root.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "os" + "strings" "github.com/rzolkos/basecamp-cli/internal/config" ) @@ -246,7 +247,13 @@ func PrintJSON(v any) error { // getProjectID returns project ID from args[0] or .basecamp.yml, plus remaining args. // If project_id comes from config, args are returned unchanged. // If project_id comes from args[0], remaining args are returned. -func getProjectID(args []string) (projectID string, remaining []string, err error) { +// +// minPositionalArgs is the number of non-flag positional args the command expects +// besides project_id (e.g. 1 for a command that needs a board_id). When no config +// is found, this is used to decide whether args[0] is a project_id or the first +// required arg. If there aren't enough positional args for both project_id and the +// command's required args, a clear error is returned instead of misinterpreting args. +func getProjectID(args []string, minPositionalArgs int) (projectID string, remaining []string, err error) { // First try to get from config configProjectID, err := config.FindProjectID() if err != nil { @@ -258,9 +265,21 @@ func getProjectID(args []string) (projectID string, remaining []string, err erro return configProjectID, args, nil } - // Need project_id from args - if len(args) < 1 { + // No config found — count non-flag positional args. + // When a --flag is encountered, skip the next arg too (its value). + positionalCount := 0 + for i := 0; i < len(args); i++ { + if strings.HasPrefix(args[i], "--") { + i++ // skip flag value + } else { + positionalCount++ + } + } + + // Need at least minPositionalArgs + 1 (for project_id) positional args + if positionalCount <= minPositionalArgs { return "", nil, errors.New("project_id required: provide as argument or create .basecamp.yml with project_id") } + return args[0], args[1:], nil } diff --git a/internal/commands/schedules.go b/internal/commands/schedules.go index 3205de3..475857b 100644 --- a/internal/commands/schedules.go +++ b/internal/commands/schedules.go @@ -73,7 +73,7 @@ type ScheduleEntryBrief struct { } func (c *ScheduleCmd) Run(args []string) error { - projectID, _, err := getProjectID(args) + projectID, _, err := getProjectID(args, 0) if err != nil { return err } @@ -137,7 +137,7 @@ type EventDetailOutput struct { } func (c *EventCmd) Run(args []string) error { - projectID, remaining, err := getProjectID(args) + projectID, remaining, err := getProjectID(args, 1) if err != nil { return err } @@ -215,7 +215,7 @@ type EventCreateOutput struct { } func (c *EventCreateCmd) Run(args []string) error { - projectID, remaining, err := getProjectID(args) + projectID, remaining, err := getProjectID(args, 0) if err != nil { return err } diff --git a/internal/commands/steps.go b/internal/commands/steps.go index cef0987..714a65a 100644 --- a/internal/commands/steps.go +++ b/internal/commands/steps.go @@ -42,7 +42,7 @@ type StepCreateOutput struct { } func (c *StepCreateCmd) Run(args []string) error { - projectID, remaining, err := getProjectID(args) + projectID, remaining, err := getProjectID(args, 1) if err != nil { return err } @@ -126,7 +126,7 @@ type StepUpdateOutput struct { } func (c *StepUpdateCmd) Run(args []string) error { - projectID, remaining, err := getProjectID(args) + projectID, remaining, err := getProjectID(args, 1) if err != nil { return err } @@ -210,7 +210,7 @@ type StepCompleteOutput struct { } func (c *StepCompleteCmd) Run(args []string) error { - projectID, remaining, err := getProjectID(args) + projectID, remaining, err := getProjectID(args, 1) if err != nil { return err } @@ -246,7 +246,7 @@ func (c *StepCompleteCmd) Run(args []string) error { type StepUncompleteCmd struct{} func (c *StepUncompleteCmd) Run(args []string) error { - projectID, remaining, err := getProjectID(args) + projectID, remaining, err := getProjectID(args, 1) if err != nil { return err } @@ -289,7 +289,7 @@ type StepRepositionOutput struct { } func (c *StepRepositionCmd) Run(args []string) error { - projectID, remaining, err := getProjectID(args) + projectID, remaining, err := getProjectID(args, 2) if err != nil { return err } diff --git a/internal/commands/todolists.go b/internal/commands/todolists.go index 199d5f8..f7e5d5e 100644 --- a/internal/commands/todolists.go +++ b/internal/commands/todolists.go @@ -63,7 +63,7 @@ type TodolistsOutput struct { } func (c *TodolistsCmd) Run(args []string) error { - projectID, _, err := getProjectID(args) + projectID, _, err := getProjectID(args, 0) if err != nil { return err } @@ -134,7 +134,7 @@ type TodolistGroupBrief struct { } func (c *TodolistGroupsCmd) Run(args []string) error { - projectID, remaining, err := getProjectID(args) + projectID, remaining, err := getProjectID(args, 1) if err != nil { return err } @@ -197,7 +197,7 @@ type TodolistGroupDetailOutput struct { } func (c *TodolistGroupCmd) Run(args []string) error { - projectID, remaining, err := getProjectID(args) + projectID, remaining, err := getProjectID(args, 1) if err != nil { return err } @@ -244,7 +244,7 @@ type TodolistGroupCreateOutput struct { } func (c *TodolistGroupCreateCmd) Run(args []string) error { - projectID, remaining, err := getProjectID(args) + projectID, remaining, err := getProjectID(args, 1) if err != nil { return err } diff --git a/internal/commands/todos.go b/internal/commands/todos.go index 25a5939..b9d6d35 100644 --- a/internal/commands/todos.go +++ b/internal/commands/todos.go @@ -37,7 +37,7 @@ type TodosOutput struct { } func (c *TodosCmd) Run(args []string) error { - projectID, remaining, err := getProjectID(args) + projectID, remaining, err := getProjectID(args, 1) if err != nil { return err } @@ -119,7 +119,7 @@ type TodoDetailOutput struct { } func (c *TodoCmd) Run(args []string) error { - projectID, remaining, err := getProjectID(args) + projectID, remaining, err := getProjectID(args, 1) if err != nil { return err } @@ -166,7 +166,7 @@ func (c *TodoCmd) Run(args []string) error { type TodoCreateCmd struct{} func (c *TodoCreateCmd) Run(args []string) error { - projectID, remaining, err := getProjectID(args) + projectID, remaining, err := getProjectID(args, 1) if err != nil { return err } @@ -237,7 +237,7 @@ func (c *TodoCreateCmd) Run(args []string) error { type TodoCompleteCmd struct{} func (c *TodoCompleteCmd) Run(args []string) error { - projectID, remaining, err := getProjectID(args) + projectID, remaining, err := getProjectID(args, 1) if err != nil { return err } @@ -270,7 +270,7 @@ func (c *TodoCompleteCmd) Run(args []string) error { type TodoUncompleteCmd struct{} func (c *TodoUncompleteCmd) Run(args []string) error { - projectID, remaining, err := getProjectID(args) + projectID, remaining, err := getProjectID(args, 1) if err != nil { return err } @@ -304,7 +304,7 @@ func (c *TodoUncompleteCmd) Run(args []string) error { type TodoRepositionCmd struct{} func (c *TodoRepositionCmd) Run(args []string) error { - projectID, remaining, err := getProjectID(args) + projectID, remaining, err := getProjectID(args, 1) if err != nil { return err } diff --git a/internal/commands/uploads.go b/internal/commands/uploads.go index e4060f7..03d708a 100644 --- a/internal/commands/uploads.go +++ b/internal/commands/uploads.go @@ -116,7 +116,7 @@ type UploadBrief struct { } func (c *UploadsCmd) Run(args []string) error { - projectID, remaining, err := getProjectID(args) + projectID, remaining, err := getProjectID(args, 1) if err != nil { return err } @@ -186,7 +186,7 @@ type UploadDetailOutput struct { } func (c *UploadViewCmd) Run(args []string) error { - projectID, remaining, err := getProjectID(args) + projectID, remaining, err := getProjectID(args, 1) if err != nil { return err }