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
6 changes: 6 additions & 0 deletions .surface
Original file line number Diff line number Diff line change
Expand Up @@ -4873,6 +4873,7 @@ FLAG basecamp todolists --quiet type=bool
FLAG basecamp todolists --stats type=bool
FLAG basecamp todolists --styled type=bool
FLAG basecamp todolists --todolist type=string
FLAG basecamp todolists --todoset type=string
FLAG basecamp todolists --verbose type=count
FLAG basecamp todolists create --account type=string
FLAG basecamp todolists create --agent type=bool
Expand All @@ -4894,6 +4895,7 @@ FLAG basecamp todolists create --quiet type=bool
FLAG basecamp todolists create --stats type=bool
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 list --account type=string
FLAG basecamp todolists list --agent type=bool
Expand All @@ -4916,6 +4918,7 @@ FLAG basecamp todolists list --quiet type=bool
FLAG basecamp todolists list --stats type=bool
FLAG basecamp todolists list --styled type=bool
FLAG basecamp todolists list --todolist type=string
FLAG basecamp todolists list --todoset type=string
FLAG basecamp todolists list --verbose type=count
FLAG basecamp todolists show --account type=string
FLAG basecamp todolists show --agent type=bool
Expand Down Expand Up @@ -4982,6 +4985,7 @@ FLAG basecamp todos --stats type=bool
FLAG basecamp todos --status type=string
FLAG basecamp todos --styled type=bool
FLAG basecamp todos --todolist type=string
FLAG basecamp todos --todoset type=string
FLAG basecamp todos --verbose type=count
FLAG basecamp todos complete --account type=string
FLAG basecamp todos complete --agent type=bool
Expand Down Expand Up @@ -5050,6 +5054,7 @@ FLAG basecamp todos list --stats type=bool
FLAG basecamp todos list --status type=string
FLAG basecamp todos list --styled type=bool
FLAG basecamp todos list --todolist type=string
FLAG basecamp todos list --todoset type=string
FLAG basecamp todos list --verbose type=count
FLAG basecamp todos position --account type=string
FLAG basecamp todos position --agent type=bool
Expand Down Expand Up @@ -5113,6 +5118,7 @@ FLAG basecamp todos sweep --quiet type=bool
FLAG basecamp todos sweep --stats type=bool
FLAG basecamp todos sweep --styled type=bool
FLAG basecamp todos sweep --todolist type=string
FLAG basecamp todos sweep --todoset type=string
FLAG basecamp todos sweep --verbose type=count
FLAG basecamp todos uncomplete --account type=string
FLAG basecamp todos uncomplete --agent type=bool
Expand Down
14 changes: 10 additions & 4 deletions internal/commands/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ func getDockToolID(ctx context.Context, app *appctx.App, projectID, dockName, ex
}
toolList = append(toolList, fmt.Sprintf("%s (ID: %d)", title, tool.ID))
}
hint := fmt.Sprintf("Specify ID directly. Available:\n - %s", strings.Join(toolList, "\n - "))
hint := fmt.Sprintf("Specify the ID directly. Available:\n - %s", strings.Join(toolList, "\n - "))
return "", &output.Error{
Code: output.CodeAmbiguous,
Message: fmt.Sprintf("Project has %d %ss", len(matches), friendlyName),
Expand Down Expand Up @@ -154,9 +154,15 @@ func ensureProject(cmd *cobra.Command, app *appctx.App) error {
return nil
}

// getTodosetID retrieves the todoset ID from a project's dock.
func getTodosetID(cmd *cobra.Command, app *appctx.App, projectID string) (string, error) {
return getDockToolID(cmd.Context(), app, projectID, "todoset", "", "todoset")
// ensureTodoset resolves the todoset ID from a project, with interactive fallback.
// If explicitTodosetID is provided (e.g. from --todoset flag), it is used directly.
// Otherwise, auto-selects when one todoset exists, or prompts when multiple exist.
func ensureTodoset(cmd *cobra.Command, app *appctx.App, projectID, explicitTodosetID string) (string, error) {
resolved, err := app.Resolve().Todoset(cmd.Context(), projectID, explicitTodosetID)
if err != nil {
return "", err
}
return resolved.ToolID, nil
}

// ensureTodolist resolves the todolist ID if not already configured.
Expand Down
33 changes: 19 additions & 14 deletions internal/commands/todolists.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
// NewTodolistsCmd creates the todolists command group.
func NewTodolistsCmd() *cobra.Command {
var project string
var todosetID string
var limit, page int
var all bool

Expand All @@ -25,7 +26,8 @@ func NewTodolistsCmd() *cobra.Command {
Long: `Manage todolists in a project.

A "todoset" is the container; "todolists" are the actual lists inside it.
Each project has one todoset containing multiple todolists.`,
Most projects have one todoset, but some may have multiple. Use --todoset
to disambiguate when needed.`,
PreRunE: func(cmd *cobra.Command, args []string) error {
app := appctx.FromContext(cmd.Context())
if app == nil {
Expand All @@ -35,27 +37,28 @@ Each project has one todoset containing multiple todolists.`,
},
RunE: func(cmd *cobra.Command, args []string) error {
// Default to list when called without subcommand
return runTodolistsList(cmd, project, limit, page, all)
return runTodolistsList(cmd, project, todosetID, limit, page, all)
},
}

cmd.PersistentFlags().StringVarP(&project, "project", "p", "", "Project ID or name")
cmd.PersistentFlags().StringVar(&project, "in", "", "Project ID (alias for --project)")
cmd.Flags().StringVarP(&todosetID, "todoset", "t", "", "Todoset ID (for projects with multiple todosets)")
cmd.Flags().IntVarP(&limit, "limit", "n", 0, "Maximum number of todolists to fetch (0 = all)")
cmd.Flags().BoolVar(&all, "all", false, "Fetch all todolists (no limit)")
cmd.Flags().IntVar(&page, "page", 0, "Fetch a single page (use --all for everything)")

cmd.AddCommand(
newTodolistsListCmd(&project),
newTodolistsListCmd(&project, &todosetID),
newTodolistsShowCmd(&project),
newTodolistsCreateCmd(&project),
newTodolistsCreateCmd(&project, &todosetID),
newTodolistsUpdateCmd(&project),
)

return cmd
}

func newTodolistsListCmd(project *string) *cobra.Command {
func newTodolistsListCmd(project, todosetID *string) *cobra.Command {
var limit, page int
var all bool

Expand All @@ -64,18 +67,19 @@ func newTodolistsListCmd(project *string) *cobra.Command {
Short: "List todolists",
Long: "List all todolists in a project.",
RunE: func(cmd *cobra.Command, args []string) error {
return runTodolistsList(cmd, *project, limit, page, all)
return runTodolistsList(cmd, *project, *todosetID, limit, page, all)
},
}

cmd.Flags().StringVarP(todosetID, "todoset", "t", "", "Todoset ID (for projects with multiple todosets)")
cmd.Flags().IntVarP(&limit, "limit", "n", 0, "Maximum number of todolists to fetch (0 = all)")
cmd.Flags().BoolVar(&all, "all", false, "Fetch all todolists (no limit)")
cmd.Flags().IntVar(&page, "page", 0, "Fetch a single page (use --all for everything)")

return cmd
}

func runTodolistsList(cmd *cobra.Command, project string, limit, page int, all bool) error {
func runTodolistsList(cmd *cobra.Command, project, todosetFlag string, limit, page int, all bool) error {
app := appctx.FromContext(cmd.Context())
if app == nil {
return fmt.Errorf("app not initialized")
Expand Down Expand Up @@ -116,8 +120,8 @@ func runTodolistsList(cmd *cobra.Command, project string, limit, page int, all b
return err
}

// Get todoset from project dock
todosetIDStr, err := getTodosetID(cmd, app, resolvedProjectID)
// Get todoset from project dock (with interactive fallback for multi-todoset projects)
todosetIDStr, err := ensureTodoset(cmd, app, resolvedProjectID, todosetFlag)
if err != nil {
return err
}
Expand Down Expand Up @@ -248,7 +252,7 @@ You can pass either a todolist ID or a Basecamp URL:
return cmd
}

func newTodolistsCreateCmd(project *string) *cobra.Command {
func newTodolistsCreateCmd(project, todosetID *string) *cobra.Command {
var name string
var description string

Expand Down Expand Up @@ -290,14 +294,14 @@ func newTodolistsCreateCmd(project *string) *cobra.Command {
return err
}

// Get todoset from project dock
todosetIDStr, err := getTodosetID(cmd, app, resolvedProjectID)
// Get todoset from project dock (with interactive fallback for multi-todoset projects)
todosetIDStr, err := ensureTodoset(cmd, app, resolvedProjectID, *todosetID)
if err != nil {
return err
}

// Parse todoset ID as int64
todosetID, err := strconv.ParseInt(todosetIDStr, 10, 64)
tsID, err := strconv.ParseInt(todosetIDStr, 10, 64)
if err != nil {
return output.ErrUsage("Invalid todoset ID")
}
Expand All @@ -309,7 +313,7 @@ func newTodolistsCreateCmd(project *string) *cobra.Command {
}

// Create todolist via SDK
todolist, err := app.Account().Todolists().Create(cmd.Context(), todosetID, req)
todolist, err := app.Account().Todolists().Create(cmd.Context(), tsID, req)
if err != nil {
return convertSDKError(err)
}
Expand All @@ -334,6 +338,7 @@ func newTodolistsCreateCmd(project *string) *cobra.Command {
},
}

cmd.Flags().StringVarP(todosetID, "todoset", "t", "", "Todoset ID (for projects with multiple todosets)")
cmd.Flags().StringVarP(&name, "name", "n", "", "Todolist name (required)")
cmd.Flags().StringVarP(&description, "description", "d", "", "Todolist description")
_ = cmd.MarkFlagRequired("name")
Expand Down
21 changes: 13 additions & 8 deletions internal/commands/todos.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
type todosListFlags struct {
project string
todolist string
todoset string
assignee string
status string
overdue bool
Expand Down Expand Up @@ -46,6 +47,7 @@ func NewTodosCmd() *cobra.Command {
// Note: can't use -a for assignee since it conflicts with global -a for account
cmd.Flags().StringVar(&flags.project, "in", "", "Project ID or name")
cmd.Flags().StringVarP(&flags.todolist, "list", "l", "", "Todolist ID")
cmd.Flags().StringVarP(&flags.todoset, "todoset", "t", "", "Todoset ID (for projects with multiple todosets)")
cmd.Flags().StringVar(&flags.assignee, "assignee", "", "Filter by assignee")
cmd.Flags().StringVarP(&flags.status, "status", "s", "", "Filter by status (completed, pending)")
cmd.Flags().BoolVar(&flags.overdue, "overdue", false, "Filter overdue todos")
Expand Down Expand Up @@ -244,6 +246,7 @@ func newTodosListCmd() *cobra.Command {
// Note: can't use -a for assignee since it conflicts with global -a for account
cmd.Flags().StringVar(&flags.project, "in", "", "Project ID or name")
cmd.Flags().StringVarP(&flags.todolist, "list", "l", "", "Todolist ID")
cmd.Flags().StringVarP(&flags.todoset, "todoset", "t", "", "Todoset ID (for projects with multiple todosets)")
cmd.Flags().StringVar(&flags.assignee, "assignee", "", "Filter by assignee")
cmd.Flags().StringVarP(&flags.status, "status", "s", "", "Filter by status (completed, pending)")
cmd.Flags().BoolVar(&flags.overdue, "overdue", false, "Filter overdue todos")
Expand Down Expand Up @@ -344,7 +347,7 @@ func runTodosList(cmd *cobra.Command, flags todosListFlags) error {
}

// Otherwise, get all todos from project's todoset
return listAllTodos(cmd, app, project, flags.assignee, flags.status, flags.overdue, flags.limit, flags.all)
return listAllTodos(cmd, app, project, flags.todoset, flags.assignee, flags.status, flags.overdue, flags.limit, flags.all)
}

func listTodosInList(cmd *cobra.Command, app *appctx.App, project, todolist, status string, limit, page int, all bool) error {
Expand Down Expand Up @@ -406,7 +409,7 @@ func listTodosInList(cmd *cobra.Command, app *appctx.App, project, todolist, sta
return app.OK(todos, respOpts...)
}

func listAllTodos(cmd *cobra.Command, app *appctx.App, project, assignee, status string, overdue bool, limit int, all bool) error {
func listAllTodos(cmd *cobra.Command, app *appctx.App, project, todosetFlag, assignee, status string, overdue bool, limit int, all bool) error {
// Resolve assignee name to ID if provided
var assigneeID int64
if assignee != "" {
Expand All @@ -417,8 +420,8 @@ func listAllTodos(cmd *cobra.Command, app *appctx.App, project, assignee, status
assigneeID, _ = strconv.ParseInt(resolvedID, 10, 64)
}

// Get todoset ID from project dock
todosetIDStr, err := getTodosetID(cmd, app, project)
// Get todoset ID from project dock (with interactive fallback for multi-todoset projects)
todosetIDStr, err := ensureTodoset(cmd, app, project, todosetFlag)
if err != nil {
return err
}
Expand Down Expand Up @@ -846,6 +849,7 @@ type SweepResult struct {

func newTodosSweepCmd() *cobra.Command {
var project string
var todoset string
var assignee string
var comment string
var overdueOnly bool
Expand Down Expand Up @@ -913,7 +917,7 @@ Examples:
project = resolvedProject

// Get matching todos using existing listAllTodos logic
matchingTodos, err := getTodosForSweep(cmd, app, project, assignee, overdueOnly)
matchingTodos, err := getTodosForSweep(cmd, app, project, todoset, assignee, overdueOnly)
if err != nil {
return err
}
Expand Down Expand Up @@ -998,6 +1002,7 @@ Examples:

cmd.Flags().StringVarP(&project, "project", "p", "", "Project ID or name")
cmd.Flags().StringVar(&project, "in", "", "Project ID (alias for --project)")
cmd.Flags().StringVarP(&todoset, "todoset", "t", "", "Todoset ID (for projects with multiple todosets)")
cmd.Flags().StringVar(&assignee, "assignee", "", "Filter by assignee")
cmd.Flags().BoolVar(&overdueOnly, "overdue", false, "Filter overdue todos")
cmd.Flags().StringVarP(&comment, "comment", "c", "", "Comment to add to matching todos")
Expand All @@ -1015,7 +1020,7 @@ Examples:
}

// getTodosForSweep gets todos matching the sweep filters.
func getTodosForSweep(cmd *cobra.Command, app *appctx.App, project, assignee string, overdue bool) ([]basecamp.Todo, error) {
func getTodosForSweep(cmd *cobra.Command, app *appctx.App, project, todosetFlag, assignee string, overdue bool) ([]basecamp.Todo, error) {
// Resolve assignee name to ID if provided
var assigneeID int64
if assignee != "" {
Expand All @@ -1026,8 +1031,8 @@ func getTodosForSweep(cmd *cobra.Command, app *appctx.App, project, assignee str
assigneeID, _ = strconv.ParseInt(resolvedID, 10, 64)
}

// Get todoset ID from project dock
todosetIDStr, err := getTodosetID(cmd, app, project)
// Get todoset ID from project dock (with interactive fallback for multi-todoset projects)
todosetIDStr, err := ensureTodoset(cmd, app, project, todosetFlag)
if err != nil {
return nil, err
}
Expand Down
Loading
Loading