diff --git a/.surface b/.surface index f2d12a98..3c68fc65 100644 --- a/.surface +++ b/.surface @@ -45,6 +45,9 @@ ARG basecamp campfire show 00 ARG basecamp campfire update 00 ARG basecamp campfire update 01 [content] ARG basecamp campfire upload 00 +ARG basecamp card-tables templatification 00 +ARG basecamp card-tables templatification 01 +ARG basecamp card-tables templatify 00 ARG basecamp cards archive 00 ARG basecamp cards column color 00 ARG basecamp cards column create 00 @@ -336,6 +339,11 @@ ARG basecamp subscriptions remove 01 [person_ids] ARG basecamp subscriptions show 00 <id|url> ARG basecamp subscriptions subscribe 00 <id|url> ARG basecamp subscriptions unsubscribe 00 <id|url> +ARG basecamp templates card-tables copy 00 <template_id> +ARG basecamp templates card-tables copy-status 00 <duplication_id> +ARG basecamp templates card-tables create 00 <name> +ARG basecamp templates card-tables duplicate 00 <template_id> +ARG basecamp templates card-tables duplication 00 <duplication_id> ARG basecamp templates construct 00 <template_id> ARG basecamp templates construction 00 <template_id> ARG basecamp templates construction 01 <construction_id> @@ -343,7 +351,19 @@ ARG basecamp templates copy 00 <template_id> ARG basecamp templates copy-status 00 <copy_id> ARG basecamp templates create 00 <name> ARG basecamp templates delete 00 <id> +ARG basecamp templates projects construct 00 <template_id> +ARG basecamp templates projects construction 00 <template_id> +ARG basecamp templates projects construction 01 <construction_id> +ARG basecamp templates projects create 00 <name> +ARG basecamp templates projects delete 00 <id> +ARG basecamp templates projects show 00 <id> +ARG basecamp templates projects update 00 <id> ARG basecamp templates show 00 <id> +ARG basecamp templates todolists copy 00 <template_id> +ARG basecamp templates todolists copy-status 00 <duplication_id> +ARG basecamp templates todolists create 00 <name> +ARG basecamp templates todolists duplicate 00 <template_id> +ARG basecamp templates todolists duplication 00 <duplication_id> ARG basecamp templates update 00 <id> ARG basecamp timeline 00 [me] ARG basecamp timesheet item 00 <id> @@ -371,6 +391,9 @@ ARG basecamp todolist position 00 <id|url>... ARG basecamp todolist reorder 00 <id|url>... ARG basecamp todolist restore 00 <id|url> ARG basecamp todolist show 00 <id|url> +ARG basecamp todolist templatification 00 <id> +ARG basecamp todolist templatification 01 <templatification_id> +ARG basecamp todolist templatify 00 <id> ARG basecamp todolist trash 00 <id|url> ARG basecamp todolist update 00 <id|url> ARG basecamp todolistgroup create 00 <name> @@ -396,6 +419,9 @@ ARG basecamp todolists position 00 <id|url>... ARG basecamp todolists reorder 00 <id|url>... ARG basecamp todolists restore 00 <id|url> ARG basecamp todolists show 00 <id|url> +ARG basecamp todolists templatification 00 <id> +ARG basecamp todolists templatification 01 <templatification_id> +ARG basecamp todolists templatify 00 <id> ARG basecamp todolists trash 00 <id|url> ARG basecamp todolists update 00 <id|url> ARG basecamp todos archive 00 <id|url> @@ -561,6 +587,9 @@ CMD basecamp campfire post CMD basecamp campfire show CMD basecamp campfire update CMD basecamp campfire upload +CMD basecamp card-tables +CMD basecamp card-tables templatification +CMD basecamp card-tables templatify CMD basecamp cards CMD basecamp cards archive CMD basecamp cards column @@ -1002,6 +1031,13 @@ CMD basecamp subscriptions show CMD basecamp subscriptions subscribe CMD basecamp subscriptions unsubscribe CMD basecamp templates +CMD basecamp templates card-tables +CMD basecamp templates card-tables copy +CMD basecamp templates card-tables copy-status +CMD basecamp templates card-tables create +CMD basecamp templates card-tables duplicate +CMD basecamp templates card-tables duplication +CMD basecamp templates card-tables list CMD basecamp templates construct CMD basecamp templates construction CMD basecamp templates copy @@ -1010,7 +1046,22 @@ CMD basecamp templates create CMD basecamp templates delete CMD basecamp templates library CMD basecamp templates list +CMD basecamp templates projects +CMD basecamp templates projects construct +CMD basecamp templates projects construction +CMD basecamp templates projects create +CMD basecamp templates projects delete +CMD basecamp templates projects list +CMD basecamp templates projects show +CMD basecamp templates projects update CMD basecamp templates show +CMD basecamp templates todolists +CMD basecamp templates todolists copy +CMD basecamp templates todolists copy-status +CMD basecamp templates todolists create +CMD basecamp templates todolists duplicate +CMD basecamp templates todolists duplication +CMD basecamp templates todolists list CMD basecamp templates update CMD basecamp timeline CMD basecamp timesheet @@ -1043,6 +1094,8 @@ CMD basecamp todolist position CMD basecamp todolist reorder CMD basecamp todolist restore CMD basecamp todolist show +CMD basecamp todolist templatification +CMD basecamp todolist templatify CMD basecamp todolist trash CMD basecamp todolist update CMD basecamp todolistgroup @@ -1070,6 +1123,8 @@ CMD basecamp todolists position CMD basecamp todolists reorder CMD basecamp todolists restore CMD basecamp todolists show +CMD basecamp todolists templatification +CMD basecamp todolists templatify CMD basecamp todolists trash CMD basecamp todolists update CMD basecamp todos @@ -2853,6 +2908,73 @@ FLAG basecamp campfire upload --stats type=bool FLAG basecamp campfire upload --styled type=bool FLAG basecamp campfire upload --todolist type=string FLAG basecamp campfire upload --verbose type=count +FLAG basecamp card-tables --account type=string +FLAG basecamp card-tables --agent type=bool +FLAG basecamp card-tables --cache-dir type=string +FLAG basecamp card-tables --count type=bool +FLAG basecamp card-tables --help type=bool +FLAG basecamp card-tables --hints type=bool +FLAG basecamp card-tables --ids-only type=bool +FLAG basecamp card-tables --in type=string +FLAG basecamp card-tables --jq type=string +FLAG basecamp card-tables --json type=bool +FLAG basecamp card-tables --markdown type=bool +FLAG basecamp card-tables --md type=bool +FLAG basecamp card-tables --no-hints type=bool +FLAG basecamp card-tables --no-stats type=bool +FLAG basecamp card-tables --profile type=string +FLAG basecamp card-tables --project type=string +FLAG basecamp card-tables --quiet type=bool +FLAG basecamp card-tables --stats type=bool +FLAG basecamp card-tables --styled type=bool +FLAG basecamp card-tables --todolist type=string +FLAG basecamp card-tables --verbose type=count +FLAG basecamp card-tables templatification --account type=string +FLAG basecamp card-tables templatification --agent type=bool +FLAG basecamp card-tables templatification --cache-dir type=string +FLAG basecamp card-tables templatification --count type=bool +FLAG basecamp card-tables templatification --help type=bool +FLAG basecamp card-tables templatification --hints type=bool +FLAG basecamp card-tables templatification --ids-only type=bool +FLAG basecamp card-tables templatification --in type=string +FLAG basecamp card-tables templatification --jq type=string +FLAG basecamp card-tables templatification --json type=bool +FLAG basecamp card-tables templatification --markdown type=bool +FLAG basecamp card-tables templatification --md type=bool +FLAG basecamp card-tables templatification --no-hints type=bool +FLAG basecamp card-tables templatification --no-stats type=bool +FLAG basecamp card-tables templatification --profile type=string +FLAG basecamp card-tables templatification --project type=string +FLAG basecamp card-tables templatification --quiet type=bool +FLAG basecamp card-tables templatification --stats type=bool +FLAG basecamp card-tables templatification --styled type=bool +FLAG basecamp card-tables templatification --todolist type=string +FLAG basecamp card-tables templatification --verbose type=count +FLAG basecamp card-tables templatify --account type=string +FLAG basecamp card-tables templatify --agent type=bool +FLAG basecamp card-tables templatify --cache-dir type=string +FLAG basecamp card-tables templatify --copy-assignments type=bool +FLAG basecamp card-tables templatify --copy-comments type=bool +FLAG basecamp card-tables templatify --count type=bool +FLAG basecamp card-tables templatify --help type=bool +FLAG basecamp card-tables templatify --hints type=bool +FLAG basecamp card-tables templatify --ids-only type=bool +FLAG basecamp card-tables templatify --in type=string +FLAG basecamp card-tables templatify --jq type=string +FLAG basecamp card-tables templatify --json type=bool +FLAG basecamp card-tables templatify --markdown type=bool +FLAG basecamp card-tables templatify --md type=bool +FLAG basecamp card-tables templatify --move-cards-to-triage type=bool +FLAG basecamp card-tables templatify --name type=string +FLAG basecamp card-tables templatify --no-hints type=bool +FLAG basecamp card-tables templatify --no-stats type=bool +FLAG basecamp card-tables templatify --profile type=string +FLAG basecamp card-tables templatify --project type=string +FLAG basecamp card-tables templatify --quiet type=bool +FLAG basecamp card-tables templatify --stats type=bool +FLAG basecamp card-tables templatify --styled type=bool +FLAG basecamp card-tables templatify --todolist type=string +FLAG basecamp card-tables templatify --verbose type=count FLAG basecamp cards --account type=string FLAG basecamp cards --agent type=bool FLAG basecamp cards --cache-dir type=string @@ -13444,6 +13566,156 @@ FLAG basecamp templates --stats type=bool FLAG basecamp templates --styled type=bool FLAG basecamp templates --todolist type=string FLAG basecamp templates --verbose type=count +FLAG basecamp templates card-tables --account type=string +FLAG basecamp templates card-tables --agent type=bool +FLAG basecamp templates card-tables --cache-dir type=string +FLAG basecamp templates card-tables --count type=bool +FLAG basecamp templates card-tables --help type=bool +FLAG basecamp templates card-tables --hints type=bool +FLAG basecamp templates card-tables --ids-only type=bool +FLAG basecamp templates card-tables --in type=string +FLAG basecamp templates card-tables --jq type=string +FLAG basecamp templates card-tables --json type=bool +FLAG basecamp templates card-tables --markdown type=bool +FLAG basecamp templates card-tables --md type=bool +FLAG basecamp templates card-tables --no-hints type=bool +FLAG basecamp templates card-tables --no-stats type=bool +FLAG basecamp templates card-tables --profile type=string +FLAG basecamp templates card-tables --project type=string +FLAG basecamp templates card-tables --quiet type=bool +FLAG basecamp templates card-tables --stats type=bool +FLAG basecamp templates card-tables --styled type=bool +FLAG basecamp templates card-tables --todolist type=string +FLAG basecamp templates card-tables --verbose type=count +FLAG basecamp templates card-tables copy --account type=string +FLAG basecamp templates card-tables copy --agent type=bool +FLAG basecamp templates card-tables copy --cache-dir type=string +FLAG basecamp templates card-tables copy --confirm-adding-people type=bool +FLAG basecamp templates card-tables copy --count type=bool +FLAG basecamp templates card-tables copy --help type=bool +FLAG basecamp templates card-tables copy --hints type=bool +FLAG basecamp templates card-tables copy --ids-only type=bool +FLAG basecamp templates card-tables copy --in type=string +FLAG basecamp templates card-tables copy --jq type=string +FLAG basecamp templates card-tables copy --json type=bool +FLAG basecamp templates card-tables copy --markdown type=bool +FLAG basecamp templates card-tables copy --md type=bool +FLAG basecamp templates card-tables copy --no-hints type=bool +FLAG basecamp templates card-tables copy --no-stats type=bool +FLAG basecamp templates card-tables copy --profile type=string +FLAG basecamp templates card-tables copy --project type=string +FLAG basecamp templates card-tables copy --quiet type=bool +FLAG basecamp templates card-tables copy --stats type=bool +FLAG basecamp templates card-tables copy --styled type=bool +FLAG basecamp templates card-tables copy --todolist type=string +FLAG basecamp templates card-tables copy --verbose type=count +FLAG basecamp templates card-tables copy-status --account type=string +FLAG basecamp templates card-tables copy-status --agent type=bool +FLAG basecamp templates card-tables copy-status --cache-dir type=string +FLAG basecamp templates card-tables copy-status --count type=bool +FLAG basecamp templates card-tables copy-status --help type=bool +FLAG basecamp templates card-tables copy-status --hints type=bool +FLAG basecamp templates card-tables copy-status --ids-only type=bool +FLAG basecamp templates card-tables copy-status --in type=string +FLAG basecamp templates card-tables copy-status --jq type=string +FLAG basecamp templates card-tables copy-status --json type=bool +FLAG basecamp templates card-tables copy-status --markdown type=bool +FLAG basecamp templates card-tables copy-status --md type=bool +FLAG basecamp templates card-tables copy-status --no-hints type=bool +FLAG basecamp templates card-tables copy-status --no-stats type=bool +FLAG basecamp templates card-tables copy-status --profile type=string +FLAG basecamp templates card-tables copy-status --project type=string +FLAG basecamp templates card-tables copy-status --quiet type=bool +FLAG basecamp templates card-tables copy-status --stats type=bool +FLAG basecamp templates card-tables copy-status --styled type=bool +FLAG basecamp templates card-tables copy-status --todolist type=string +FLAG basecamp templates card-tables copy-status --verbose type=count +FLAG basecamp templates card-tables create --account type=string +FLAG basecamp templates card-tables create --agent type=bool +FLAG basecamp templates card-tables create --cache-dir type=string +FLAG basecamp templates card-tables create --count type=bool +FLAG basecamp templates card-tables create --help type=bool +FLAG basecamp templates card-tables create --hints type=bool +FLAG basecamp templates card-tables create --ids-only type=bool +FLAG basecamp templates card-tables create --in type=string +FLAG basecamp templates card-tables create --jq type=string +FLAG basecamp templates card-tables create --json type=bool +FLAG basecamp templates card-tables create --markdown type=bool +FLAG basecamp templates card-tables create --md type=bool +FLAG basecamp templates card-tables create --name type=string +FLAG basecamp templates card-tables create --no-hints type=bool +FLAG basecamp templates card-tables create --no-stats type=bool +FLAG basecamp templates card-tables create --profile type=string +FLAG basecamp templates card-tables create --project type=string +FLAG basecamp templates card-tables create --quiet type=bool +FLAG basecamp templates card-tables create --stats type=bool +FLAG basecamp templates card-tables create --styled type=bool +FLAG basecamp templates card-tables create --todolist type=string +FLAG basecamp templates card-tables create --verbose type=count +FLAG basecamp templates card-tables duplicate --account type=string +FLAG basecamp templates card-tables duplicate --agent type=bool +FLAG basecamp templates card-tables duplicate --cache-dir type=string +FLAG basecamp templates card-tables duplicate --confirm-adding-people type=bool +FLAG basecamp templates card-tables duplicate --count type=bool +FLAG basecamp templates card-tables duplicate --help type=bool +FLAG basecamp templates card-tables duplicate --hints type=bool +FLAG basecamp templates card-tables duplicate --ids-only type=bool +FLAG basecamp templates card-tables duplicate --in type=string +FLAG basecamp templates card-tables duplicate --jq type=string +FLAG basecamp templates card-tables duplicate --json type=bool +FLAG basecamp templates card-tables duplicate --markdown type=bool +FLAG basecamp templates card-tables duplicate --md type=bool +FLAG basecamp templates card-tables duplicate --no-hints type=bool +FLAG basecamp templates card-tables duplicate --no-stats type=bool +FLAG basecamp templates card-tables duplicate --profile type=string +FLAG basecamp templates card-tables duplicate --project type=string +FLAG basecamp templates card-tables duplicate --quiet type=bool +FLAG basecamp templates card-tables duplicate --stats type=bool +FLAG basecamp templates card-tables duplicate --styled type=bool +FLAG basecamp templates card-tables duplicate --todolist type=string +FLAG basecamp templates card-tables duplicate --verbose type=count +FLAG basecamp templates card-tables duplication --account type=string +FLAG basecamp templates card-tables duplication --agent type=bool +FLAG basecamp templates card-tables duplication --cache-dir type=string +FLAG basecamp templates card-tables duplication --count type=bool +FLAG basecamp templates card-tables duplication --help type=bool +FLAG basecamp templates card-tables duplication --hints type=bool +FLAG basecamp templates card-tables duplication --ids-only type=bool +FLAG basecamp templates card-tables duplication --in type=string +FLAG basecamp templates card-tables duplication --jq type=string +FLAG basecamp templates card-tables duplication --json type=bool +FLAG basecamp templates card-tables duplication --markdown type=bool +FLAG basecamp templates card-tables duplication --md type=bool +FLAG basecamp templates card-tables duplication --no-hints type=bool +FLAG basecamp templates card-tables duplication --no-stats type=bool +FLAG basecamp templates card-tables duplication --profile type=string +FLAG basecamp templates card-tables duplication --project type=string +FLAG basecamp templates card-tables duplication --quiet type=bool +FLAG basecamp templates card-tables duplication --stats type=bool +FLAG basecamp templates card-tables duplication --styled type=bool +FLAG basecamp templates card-tables duplication --todolist type=string +FLAG basecamp templates card-tables duplication --verbose type=count +FLAG basecamp templates card-tables list --account type=string +FLAG basecamp templates card-tables list --agent type=bool +FLAG basecamp templates card-tables list --cache-dir type=string +FLAG basecamp templates card-tables list --count type=bool +FLAG basecamp templates card-tables list --help type=bool +FLAG basecamp templates card-tables list --hints type=bool +FLAG basecamp templates card-tables list --ids-only type=bool +FLAG basecamp templates card-tables list --in type=string +FLAG basecamp templates card-tables list --jq type=string +FLAG basecamp templates card-tables list --json type=bool +FLAG basecamp templates card-tables list --markdown type=bool +FLAG basecamp templates card-tables list --md type=bool +FLAG basecamp templates card-tables list --no-hints type=bool +FLAG basecamp templates card-tables list --no-stats type=bool +FLAG basecamp templates card-tables list --profile type=string +FLAG basecamp templates card-tables list --project type=string +FLAG basecamp templates card-tables list --quiet type=bool +FLAG basecamp templates card-tables list --stats type=bool +FLAG basecamp templates card-tables list --styled type=bool +FLAG basecamp templates card-tables list --todolist type=string +FLAG basecamp templates card-tables list --verbose type=count FLAG basecamp templates construct --account type=string FLAG basecamp templates construct --agent type=bool FLAG basecamp templates construct --cache-dir type=string @@ -13622,6 +13894,185 @@ FLAG basecamp templates list --status type=string FLAG basecamp templates list --styled type=bool FLAG basecamp templates list --todolist type=string FLAG basecamp templates list --verbose type=count +FLAG basecamp templates projects --account type=string +FLAG basecamp templates projects --agent type=bool +FLAG basecamp templates projects --cache-dir type=string +FLAG basecamp templates projects --count type=bool +FLAG basecamp templates projects --help type=bool +FLAG basecamp templates projects --hints type=bool +FLAG basecamp templates projects --ids-only type=bool +FLAG basecamp templates projects --in type=string +FLAG basecamp templates projects --jq type=string +FLAG basecamp templates projects --json type=bool +FLAG basecamp templates projects --markdown type=bool +FLAG basecamp templates projects --md type=bool +FLAG basecamp templates projects --no-hints type=bool +FLAG basecamp templates projects --no-stats type=bool +FLAG basecamp templates projects --profile type=string +FLAG basecamp templates projects --project type=string +FLAG basecamp templates projects --quiet type=bool +FLAG basecamp templates projects --stats type=bool +FLAG basecamp templates projects --styled type=bool +FLAG basecamp templates projects --todolist type=string +FLAG basecamp templates projects --verbose type=count +FLAG basecamp templates projects construct --account type=string +FLAG basecamp templates projects construct --agent type=bool +FLAG basecamp templates projects construct --cache-dir type=string +FLAG basecamp templates projects construct --count type=bool +FLAG basecamp templates projects construct --desc type=string +FLAG basecamp templates projects construct --description type=string +FLAG basecamp templates projects construct --help type=bool +FLAG basecamp templates projects construct --hints type=bool +FLAG basecamp templates projects construct --ids-only type=bool +FLAG basecamp templates projects construct --in type=string +FLAG basecamp templates projects construct --jq type=string +FLAG basecamp templates projects construct --json type=bool +FLAG basecamp templates projects construct --markdown type=bool +FLAG basecamp templates projects construct --md type=bool +FLAG basecamp templates projects construct --name type=string +FLAG basecamp templates projects construct --no-hints type=bool +FLAG basecamp templates projects construct --no-stats type=bool +FLAG basecamp templates projects construct --profile type=string +FLAG basecamp templates projects construct --project type=string +FLAG basecamp templates projects construct --quiet type=bool +FLAG basecamp templates projects construct --start-date type=string +FLAG basecamp templates projects construct --stats type=bool +FLAG basecamp templates projects construct --styled type=bool +FLAG basecamp templates projects construct --todolist type=string +FLAG basecamp templates projects construct --verbose type=count +FLAG basecamp templates projects construction --account type=string +FLAG basecamp templates projects construction --agent type=bool +FLAG basecamp templates projects construction --cache-dir type=string +FLAG basecamp templates projects construction --count type=bool +FLAG basecamp templates projects construction --help type=bool +FLAG basecamp templates projects construction --hints type=bool +FLAG basecamp templates projects construction --ids-only type=bool +FLAG basecamp templates projects construction --in type=string +FLAG basecamp templates projects construction --jq type=string +FLAG basecamp templates projects construction --json type=bool +FLAG basecamp templates projects construction --markdown type=bool +FLAG basecamp templates projects construction --md type=bool +FLAG basecamp templates projects construction --no-hints type=bool +FLAG basecamp templates projects construction --no-stats type=bool +FLAG basecamp templates projects construction --profile type=string +FLAG basecamp templates projects construction --project type=string +FLAG basecamp templates projects construction --quiet type=bool +FLAG basecamp templates projects construction --stats type=bool +FLAG basecamp templates projects construction --styled type=bool +FLAG basecamp templates projects construction --todolist type=string +FLAG basecamp templates projects construction --verbose type=count +FLAG basecamp templates projects create --account type=string +FLAG basecamp templates projects create --agent type=bool +FLAG basecamp templates projects create --cache-dir type=string +FLAG basecamp templates projects create --count type=bool +FLAG basecamp templates projects create --desc type=string +FLAG basecamp templates projects create --description type=string +FLAG basecamp templates projects create --help type=bool +FLAG basecamp templates projects create --hints type=bool +FLAG basecamp templates projects create --ids-only type=bool +FLAG basecamp templates projects create --in type=string +FLAG basecamp templates projects create --jq type=string +FLAG basecamp templates projects create --json type=bool +FLAG basecamp templates projects create --markdown type=bool +FLAG basecamp templates projects create --md type=bool +FLAG basecamp templates projects create --name type=string +FLAG basecamp templates projects create --no-hints type=bool +FLAG basecamp templates projects create --no-stats type=bool +FLAG basecamp templates projects create --profile type=string +FLAG basecamp templates projects create --project type=string +FLAG basecamp templates projects create --quiet type=bool +FLAG basecamp templates projects create --stats type=bool +FLAG basecamp templates projects create --styled type=bool +FLAG basecamp templates projects create --todolist type=string +FLAG basecamp templates projects create --verbose type=count +FLAG basecamp templates projects delete --account type=string +FLAG basecamp templates projects delete --agent type=bool +FLAG basecamp templates projects delete --cache-dir type=string +FLAG basecamp templates projects delete --count type=bool +FLAG basecamp templates projects delete --help type=bool +FLAG basecamp templates projects delete --hints type=bool +FLAG basecamp templates projects delete --ids-only type=bool +FLAG basecamp templates projects delete --in type=string +FLAG basecamp templates projects delete --jq type=string +FLAG basecamp templates projects delete --json type=bool +FLAG basecamp templates projects delete --markdown type=bool +FLAG basecamp templates projects delete --md type=bool +FLAG basecamp templates projects delete --no-hints type=bool +FLAG basecamp templates projects delete --no-stats type=bool +FLAG basecamp templates projects delete --profile type=string +FLAG basecamp templates projects delete --project type=string +FLAG basecamp templates projects delete --quiet type=bool +FLAG basecamp templates projects delete --stats type=bool +FLAG basecamp templates projects delete --styled type=bool +FLAG basecamp templates projects delete --todolist type=string +FLAG basecamp templates projects delete --verbose type=count +FLAG basecamp templates projects list --account type=string +FLAG basecamp templates projects list --agent type=bool +FLAG basecamp templates projects list --cache-dir type=string +FLAG basecamp templates projects list --count type=bool +FLAG basecamp templates projects list --help type=bool +FLAG basecamp templates projects list --hints type=bool +FLAG basecamp templates projects list --ids-only type=bool +FLAG basecamp templates projects list --in type=string +FLAG basecamp templates projects list --jq type=string +FLAG basecamp templates projects list --json type=bool +FLAG basecamp templates projects list --markdown type=bool +FLAG basecamp templates projects list --md type=bool +FLAG basecamp templates projects list --no-hints type=bool +FLAG basecamp templates projects list --no-stats type=bool +FLAG basecamp templates projects list --profile type=string +FLAG basecamp templates projects list --project type=string +FLAG basecamp templates projects list --quiet type=bool +FLAG basecamp templates projects list --stats type=bool +FLAG basecamp templates projects list --status type=string +FLAG basecamp templates projects list --styled type=bool +FLAG basecamp templates projects list --todolist type=string +FLAG basecamp templates projects list --verbose type=count +FLAG basecamp templates projects show --account type=string +FLAG basecamp templates projects show --agent type=bool +FLAG basecamp templates projects show --cache-dir type=string +FLAG basecamp templates projects show --count type=bool +FLAG basecamp templates projects show --help type=bool +FLAG basecamp templates projects show --hints type=bool +FLAG basecamp templates projects show --ids-only type=bool +FLAG basecamp templates projects show --in type=string +FLAG basecamp templates projects show --jq type=string +FLAG basecamp templates projects show --json type=bool +FLAG basecamp templates projects show --markdown type=bool +FLAG basecamp templates projects show --md type=bool +FLAG basecamp templates projects show --no-hints type=bool +FLAG basecamp templates projects show --no-stats type=bool +FLAG basecamp templates projects show --profile type=string +FLAG basecamp templates projects show --project type=string +FLAG basecamp templates projects show --quiet type=bool +FLAG basecamp templates projects show --stats type=bool +FLAG basecamp templates projects show --styled type=bool +FLAG basecamp templates projects show --todolist type=string +FLAG basecamp templates projects show --verbose type=count +FLAG basecamp templates projects update --account type=string +FLAG basecamp templates projects update --agent type=bool +FLAG basecamp templates projects update --cache-dir type=string +FLAG basecamp templates projects update --count type=bool +FLAG basecamp templates projects update --desc type=string +FLAG basecamp templates projects update --description type=string +FLAG basecamp templates projects update --help type=bool +FLAG basecamp templates projects update --hints type=bool +FLAG basecamp templates projects update --ids-only type=bool +FLAG basecamp templates projects update --in type=string +FLAG basecamp templates projects update --jq type=string +FLAG basecamp templates projects update --json type=bool +FLAG basecamp templates projects update --markdown type=bool +FLAG basecamp templates projects update --md type=bool +FLAG basecamp templates projects update --name type=string +FLAG basecamp templates projects update --no-hints type=bool +FLAG basecamp templates projects update --no-stats type=bool +FLAG basecamp templates projects update --profile type=string +FLAG basecamp templates projects update --project type=string +FLAG basecamp templates projects update --quiet type=bool +FLAG basecamp templates projects update --stats type=bool +FLAG basecamp templates projects update --styled type=bool +FLAG basecamp templates projects update --todolist type=string +FLAG basecamp templates projects update --verbose type=count FLAG basecamp templates show --account type=string FLAG basecamp templates show --agent type=bool FLAG basecamp templates show --cache-dir type=string @@ -13643,6 +14094,160 @@ FLAG basecamp templates show --stats type=bool FLAG basecamp templates show --styled type=bool FLAG basecamp templates show --todolist type=string FLAG basecamp templates show --verbose type=count +FLAG basecamp templates todolists --account type=string +FLAG basecamp templates todolists --agent type=bool +FLAG basecamp templates todolists --cache-dir type=string +FLAG basecamp templates todolists --count type=bool +FLAG basecamp templates todolists --help type=bool +FLAG basecamp templates todolists --hints type=bool +FLAG basecamp templates todolists --ids-only type=bool +FLAG basecamp templates todolists --in type=string +FLAG basecamp templates todolists --jq type=string +FLAG basecamp templates todolists --json type=bool +FLAG basecamp templates todolists --markdown type=bool +FLAG basecamp templates todolists --md type=bool +FLAG basecamp templates todolists --no-hints type=bool +FLAG basecamp templates todolists --no-stats type=bool +FLAG basecamp templates todolists --profile type=string +FLAG basecamp templates todolists --project type=string +FLAG basecamp templates todolists --quiet type=bool +FLAG basecamp templates todolists --stats type=bool +FLAG basecamp templates todolists --styled type=bool +FLAG basecamp templates todolists --todolist type=string +FLAG basecamp templates todolists --verbose type=count +FLAG basecamp templates todolists copy --account type=string +FLAG basecamp templates todolists copy --agent type=bool +FLAG basecamp templates todolists copy --cache-dir type=string +FLAG basecamp templates todolists copy --confirm-adding-people type=bool +FLAG basecamp templates todolists copy --count type=bool +FLAG basecamp templates todolists copy --help type=bool +FLAG basecamp templates todolists copy --hints type=bool +FLAG basecamp templates todolists copy --ids-only type=bool +FLAG basecamp templates todolists copy --in type=string +FLAG basecamp templates todolists copy --jq type=string +FLAG basecamp templates todolists copy --json type=bool +FLAG basecamp templates todolists copy --markdown type=bool +FLAG basecamp templates todolists copy --md type=bool +FLAG basecamp templates todolists copy --no-hints type=bool +FLAG basecamp templates todolists copy --no-stats type=bool +FLAG basecamp templates todolists copy --profile type=string +FLAG basecamp templates todolists copy --project type=string +FLAG basecamp templates todolists copy --quiet type=bool +FLAG basecamp templates todolists copy --stats type=bool +FLAG basecamp templates todolists copy --styled type=bool +FLAG basecamp templates todolists copy --todolist type=string +FLAG basecamp templates todolists copy --todoset type=string +FLAG basecamp templates todolists copy --verbose type=count +FLAG basecamp templates todolists copy-status --account type=string +FLAG basecamp templates todolists copy-status --agent type=bool +FLAG basecamp templates todolists copy-status --cache-dir type=string +FLAG basecamp templates todolists copy-status --count type=bool +FLAG basecamp templates todolists copy-status --help type=bool +FLAG basecamp templates todolists copy-status --hints type=bool +FLAG basecamp templates todolists copy-status --ids-only type=bool +FLAG basecamp templates todolists copy-status --in type=string +FLAG basecamp templates todolists copy-status --jq type=string +FLAG basecamp templates todolists copy-status --json type=bool +FLAG basecamp templates todolists copy-status --markdown type=bool +FLAG basecamp templates todolists copy-status --md type=bool +FLAG basecamp templates todolists copy-status --no-hints type=bool +FLAG basecamp templates todolists copy-status --no-stats type=bool +FLAG basecamp templates todolists copy-status --profile type=string +FLAG basecamp templates todolists copy-status --project type=string +FLAG basecamp templates todolists copy-status --quiet type=bool +FLAG basecamp templates todolists copy-status --stats type=bool +FLAG basecamp templates todolists copy-status --styled type=bool +FLAG basecamp templates todolists copy-status --todolist type=string +FLAG basecamp templates todolists copy-status --verbose type=count +FLAG basecamp templates todolists create --account type=string +FLAG basecamp templates todolists create --agent type=bool +FLAG basecamp templates todolists create --cache-dir type=string +FLAG basecamp templates todolists create --count type=bool +FLAG basecamp templates todolists create --desc type=string +FLAG basecamp templates todolists create --description type=string +FLAG basecamp templates todolists create --help type=bool +FLAG basecamp templates todolists create --hints type=bool +FLAG basecamp templates todolists create --ids-only type=bool +FLAG basecamp templates todolists create --in type=string +FLAG basecamp templates todolists create --jq type=string +FLAG basecamp templates todolists create --json type=bool +FLAG basecamp templates todolists create --markdown type=bool +FLAG basecamp templates todolists create --md type=bool +FLAG basecamp templates todolists create --name type=string +FLAG basecamp templates todolists create --no-hints type=bool +FLAG basecamp templates todolists create --no-stats type=bool +FLAG basecamp templates todolists create --profile type=string +FLAG basecamp templates todolists create --project type=string +FLAG basecamp templates todolists create --quiet type=bool +FLAG basecamp templates todolists create --stats type=bool +FLAG basecamp templates todolists create --styled type=bool +FLAG basecamp templates todolists create --todolist type=string +FLAG basecamp templates todolists create --verbose type=count +FLAG basecamp templates todolists duplicate --account type=string +FLAG basecamp templates todolists duplicate --agent type=bool +FLAG basecamp templates todolists duplicate --cache-dir type=string +FLAG basecamp templates todolists duplicate --confirm-adding-people type=bool +FLAG basecamp templates todolists duplicate --count type=bool +FLAG basecamp templates todolists duplicate --help type=bool +FLAG basecamp templates todolists duplicate --hints type=bool +FLAG basecamp templates todolists duplicate --ids-only type=bool +FLAG basecamp templates todolists duplicate --in type=string +FLAG basecamp templates todolists duplicate --jq type=string +FLAG basecamp templates todolists duplicate --json type=bool +FLAG basecamp templates todolists duplicate --markdown type=bool +FLAG basecamp templates todolists duplicate --md type=bool +FLAG basecamp templates todolists duplicate --no-hints type=bool +FLAG basecamp templates todolists duplicate --no-stats type=bool +FLAG basecamp templates todolists duplicate --profile type=string +FLAG basecamp templates todolists duplicate --project type=string +FLAG basecamp templates todolists duplicate --quiet type=bool +FLAG basecamp templates todolists duplicate --stats type=bool +FLAG basecamp templates todolists duplicate --styled type=bool +FLAG basecamp templates todolists duplicate --todolist type=string +FLAG basecamp templates todolists duplicate --todoset type=string +FLAG basecamp templates todolists duplicate --verbose type=count +FLAG basecamp templates todolists duplication --account type=string +FLAG basecamp templates todolists duplication --agent type=bool +FLAG basecamp templates todolists duplication --cache-dir type=string +FLAG basecamp templates todolists duplication --count type=bool +FLAG basecamp templates todolists duplication --help type=bool +FLAG basecamp templates todolists duplication --hints type=bool +FLAG basecamp templates todolists duplication --ids-only type=bool +FLAG basecamp templates todolists duplication --in type=string +FLAG basecamp templates todolists duplication --jq type=string +FLAG basecamp templates todolists duplication --json type=bool +FLAG basecamp templates todolists duplication --markdown type=bool +FLAG basecamp templates todolists duplication --md type=bool +FLAG basecamp templates todolists duplication --no-hints type=bool +FLAG basecamp templates todolists duplication --no-stats type=bool +FLAG basecamp templates todolists duplication --profile type=string +FLAG basecamp templates todolists duplication --project type=string +FLAG basecamp templates todolists duplication --quiet type=bool +FLAG basecamp templates todolists duplication --stats type=bool +FLAG basecamp templates todolists duplication --styled type=bool +FLAG basecamp templates todolists duplication --todolist type=string +FLAG basecamp templates todolists duplication --verbose type=count +FLAG basecamp templates todolists list --account type=string +FLAG basecamp templates todolists list --agent type=bool +FLAG basecamp templates todolists list --cache-dir type=string +FLAG basecamp templates todolists list --count type=bool +FLAG basecamp templates todolists list --help type=bool +FLAG basecamp templates todolists list --hints type=bool +FLAG basecamp templates todolists list --ids-only type=bool +FLAG basecamp templates todolists list --in type=string +FLAG basecamp templates todolists list --jq type=string +FLAG basecamp templates todolists list --json type=bool +FLAG basecamp templates todolists list --markdown type=bool +FLAG basecamp templates todolists list --md type=bool +FLAG basecamp templates todolists list --no-hints type=bool +FLAG basecamp templates todolists list --no-stats type=bool +FLAG basecamp templates todolists list --profile type=string +FLAG basecamp templates todolists list --project type=string +FLAG basecamp templates todolists list --quiet type=bool +FLAG basecamp templates todolists list --stats type=bool +FLAG basecamp templates todolists list --styled type=bool +FLAG basecamp templates todolists list --todolist type=string +FLAG basecamp templates todolists list --verbose type=count FLAG basecamp templates update --account type=string FLAG basecamp templates update --agent type=bool FLAG basecamp templates update --cache-dir type=string @@ -14390,6 +14995,51 @@ FLAG basecamp todolist show --stats type=bool FLAG basecamp todolist show --styled type=bool FLAG basecamp todolist show --todolist type=string FLAG basecamp todolist show --verbose type=count +FLAG basecamp todolist templatification --account type=string +FLAG basecamp todolist templatification --agent type=bool +FLAG basecamp todolist templatification --cache-dir type=string +FLAG basecamp todolist templatification --count type=bool +FLAG basecamp todolist templatification --help type=bool +FLAG basecamp todolist templatification --hints type=bool +FLAG basecamp todolist templatification --ids-only type=bool +FLAG basecamp todolist templatification --in type=string +FLAG basecamp todolist templatification --jq type=string +FLAG basecamp todolist templatification --json type=bool +FLAG basecamp todolist templatification --markdown type=bool +FLAG basecamp todolist templatification --md type=bool +FLAG basecamp todolist templatification --no-hints type=bool +FLAG basecamp todolist templatification --no-stats type=bool +FLAG basecamp todolist templatification --profile type=string +FLAG basecamp todolist templatification --project type=string +FLAG basecamp todolist templatification --quiet type=bool +FLAG basecamp todolist templatification --stats type=bool +FLAG basecamp todolist templatification --styled type=bool +FLAG basecamp todolist templatification --todolist type=string +FLAG basecamp todolist templatification --verbose type=count +FLAG basecamp todolist templatify --account type=string +FLAG basecamp todolist templatify --agent type=bool +FLAG basecamp todolist templatify --cache-dir type=string +FLAG basecamp todolist templatify --copy-assignments type=bool +FLAG basecamp todolist templatify --copy-comments type=bool +FLAG basecamp todolist templatify --count type=bool +FLAG basecamp todolist templatify --help type=bool +FLAG basecamp todolist templatify --hints type=bool +FLAG basecamp todolist templatify --ids-only type=bool +FLAG basecamp todolist templatify --in type=string +FLAG basecamp todolist templatify --jq type=string +FLAG basecamp todolist templatify --json type=bool +FLAG basecamp todolist templatify --markdown type=bool +FLAG basecamp todolist templatify --md type=bool +FLAG basecamp todolist templatify --name type=string +FLAG basecamp todolist templatify --no-hints type=bool +FLAG basecamp todolist templatify --no-stats type=bool +FLAG basecamp todolist templatify --profile type=string +FLAG basecamp todolist templatify --project type=string +FLAG basecamp todolist templatify --quiet type=bool +FLAG basecamp todolist templatify --stats type=bool +FLAG basecamp todolist templatify --styled type=bool +FLAG basecamp todolist templatify --todolist type=string +FLAG basecamp todolist templatify --verbose type=count FLAG basecamp todolist trash --account type=string FLAG basecamp todolist trash --agent type=bool FLAG basecamp todolist trash --cache-dir type=string @@ -15002,6 +15652,51 @@ FLAG basecamp todolists show --stats type=bool FLAG basecamp todolists show --styled type=bool FLAG basecamp todolists show --todolist type=string FLAG basecamp todolists show --verbose type=count +FLAG basecamp todolists templatification --account type=string +FLAG basecamp todolists templatification --agent type=bool +FLAG basecamp todolists templatification --cache-dir type=string +FLAG basecamp todolists templatification --count type=bool +FLAG basecamp todolists templatification --help type=bool +FLAG basecamp todolists templatification --hints type=bool +FLAG basecamp todolists templatification --ids-only type=bool +FLAG basecamp todolists templatification --in type=string +FLAG basecamp todolists templatification --jq type=string +FLAG basecamp todolists templatification --json type=bool +FLAG basecamp todolists templatification --markdown type=bool +FLAG basecamp todolists templatification --md type=bool +FLAG basecamp todolists templatification --no-hints type=bool +FLAG basecamp todolists templatification --no-stats type=bool +FLAG basecamp todolists templatification --profile type=string +FLAG basecamp todolists templatification --project type=string +FLAG basecamp todolists templatification --quiet type=bool +FLAG basecamp todolists templatification --stats type=bool +FLAG basecamp todolists templatification --styled type=bool +FLAG basecamp todolists templatification --todolist type=string +FLAG basecamp todolists templatification --verbose type=count +FLAG basecamp todolists templatify --account type=string +FLAG basecamp todolists templatify --agent type=bool +FLAG basecamp todolists templatify --cache-dir type=string +FLAG basecamp todolists templatify --copy-assignments type=bool +FLAG basecamp todolists templatify --copy-comments type=bool +FLAG basecamp todolists templatify --count type=bool +FLAG basecamp todolists templatify --help type=bool +FLAG basecamp todolists templatify --hints type=bool +FLAG basecamp todolists templatify --ids-only type=bool +FLAG basecamp todolists templatify --in type=string +FLAG basecamp todolists templatify --jq type=string +FLAG basecamp todolists templatify --json type=bool +FLAG basecamp todolists templatify --markdown type=bool +FLAG basecamp todolists templatify --md type=bool +FLAG basecamp todolists templatify --name type=string +FLAG basecamp todolists templatify --no-hints type=bool +FLAG basecamp todolists templatify --no-stats type=bool +FLAG basecamp todolists templatify --profile type=string +FLAG basecamp todolists templatify --project type=string +FLAG basecamp todolists templatify --quiet type=bool +FLAG basecamp todolists templatify --stats type=bool +FLAG basecamp todolists templatify --styled type=bool +FLAG basecamp todolists templatify --todolist type=string +FLAG basecamp todolists templatify --verbose type=count FLAG basecamp todolists trash --account type=string FLAG basecamp todolists trash --agent type=bool FLAG basecamp todolists trash --cache-dir type=string @@ -18250,6 +18945,9 @@ SUB basecamp campfire post SUB basecamp campfire show SUB basecamp campfire update SUB basecamp campfire upload +SUB basecamp card-tables +SUB basecamp card-tables templatification +SUB basecamp card-tables templatify SUB basecamp cards SUB basecamp cards archive SUB basecamp cards column @@ -18691,6 +19389,13 @@ SUB basecamp subscriptions show SUB basecamp subscriptions subscribe SUB basecamp subscriptions unsubscribe SUB basecamp templates +SUB basecamp templates card-tables +SUB basecamp templates card-tables copy +SUB basecamp templates card-tables copy-status +SUB basecamp templates card-tables create +SUB basecamp templates card-tables duplicate +SUB basecamp templates card-tables duplication +SUB basecamp templates card-tables list SUB basecamp templates construct SUB basecamp templates construction SUB basecamp templates copy @@ -18699,7 +19404,22 @@ SUB basecamp templates create SUB basecamp templates delete SUB basecamp templates library SUB basecamp templates list +SUB basecamp templates projects +SUB basecamp templates projects construct +SUB basecamp templates projects construction +SUB basecamp templates projects create +SUB basecamp templates projects delete +SUB basecamp templates projects list +SUB basecamp templates projects show +SUB basecamp templates projects update SUB basecamp templates show +SUB basecamp templates todolists +SUB basecamp templates todolists copy +SUB basecamp templates todolists copy-status +SUB basecamp templates todolists create +SUB basecamp templates todolists duplicate +SUB basecamp templates todolists duplication +SUB basecamp templates todolists list SUB basecamp templates update SUB basecamp timeline SUB basecamp timesheet @@ -18732,6 +19452,8 @@ SUB basecamp todolist position SUB basecamp todolist reorder SUB basecamp todolist restore SUB basecamp todolist show +SUB basecamp todolist templatification +SUB basecamp todolist templatify SUB basecamp todolist trash SUB basecamp todolist update SUB basecamp todolistgroup @@ -18759,6 +19481,8 @@ SUB basecamp todolists position SUB basecamp todolists reorder SUB basecamp todolists restore SUB basecamp todolists show +SUB basecamp todolists templatification +SUB basecamp todolists templatify SUB basecamp todolists trash SUB basecamp todolists update SUB basecamp todos diff --git a/e2e/smoke/smoke_account.bats b/e2e/smoke/smoke_account.bats index f5168b40..79f2bbba 100644 --- a/e2e/smoke/smoke_account.bats +++ b/e2e/smoke/smoke_account.bats @@ -28,6 +28,19 @@ setup_file() { assert_json_value '.ok' 'true' } +@test "templates projects list returns templates" { + run_smoke basecamp templates projects list --json + assert_success + assert_json_value '.ok' 'true' +} + +@test "templates todolists list returns to-do list templates" { + run_smoke basecamp templates todolists list --json + [[ "$status" -ne 0 ]] && mark_unverifiable "Template library not available in this account" + assert_success + assert_json_value '.ok' 'true' +} + @test "people show returns person detail" { run_smoke basecamp people show me --json assert_success @@ -102,3 +115,23 @@ setup_file() { @test "account is out of scope" { mark_out_of_scope "Alias for accounts — tested via canonical form" } + +@test "templates projects show returns template detail" { + local out + out=$(basecamp templates projects list --json 2>/dev/null) || mark_unverifiable "Cannot list templates" + local tmpl_id + tmpl_id=$(echo "$out" | jq -r '.data[0].id // empty') + [[ -n "$tmpl_id" ]] || mark_unverifiable "No templates found" + + run_smoke basecamp templates projects show "$tmpl_id" --json + assert_success + assert_json_value '.ok' 'true' + assert_json_not_null '.data.id' +} + +@test "templates card-tables list returns card table templates" { + run_smoke basecamp templates card-tables list --json + [[ "$status" -ne 0 ]] && mark_unverifiable "Card table template library not available in this account" + assert_success + assert_json_value '.ok' 'true' +} diff --git a/e2e/smoke/smoke_lifecycle.bats b/e2e/smoke/smoke_lifecycle.bats index 58bff07b..d09fad2a 100644 --- a/e2e/smoke/smoke_lifecycle.bats +++ b/e2e/smoke/smoke_lifecycle.bats @@ -117,6 +117,82 @@ load smoke_helper mark_out_of_scope "Account-wide template mutation" } +@test "templates projects construct is out of scope" { + mark_out_of_scope "Creates project from template — account-wide mutation" +} + +@test "templates projects construction is out of scope" { + mark_out_of_scope "Depends on templates projects construct (OOS)" +} + +@test "templates projects create is out of scope" { + mark_out_of_scope "Account-wide template mutation" +} + +@test "templates projects update is out of scope" { + mark_out_of_scope "Account-wide template mutation" +} + +@test "templates projects delete is out of scope" { + mark_out_of_scope "Account-wide template mutation" +} + +@test "templates todolists duplicate is out of scope" { + mark_out_of_scope "Duplicates a to-do list into a project and may grant project access" +} + +@test "templates todolists duplication is out of scope" { + mark_out_of_scope "Depends on templates todolists duplicate (OOS)" +} + +@test "templates todolists copy is out of scope" { + mark_out_of_scope "Alias of templates todolists duplicate (OOS)" +} + +@test "templates todolists copy-status is out of scope" { + mark_out_of_scope "Alias of templates todolists duplication (OOS)" +} + +@test "templates card-tables create is out of scope" { + mark_out_of_scope "Account-wide template mutation" +} + +@test "templates card-tables duplicate is out of scope" { + mark_out_of_scope "Duplicates a board into a project and may grant project access" +} + +@test "templates card-tables duplication is out of scope" { + mark_out_of_scope "Depends on templates card-tables duplicate (OOS)" +} + +@test "templates card-tables copy is out of scope" { + mark_out_of_scope "Alias of templates card-tables duplicate (OOS)" +} + +@test "templates card-tables copy-status is out of scope" { + mark_out_of_scope "Alias of templates card-tables duplication (OOS)" +} + +@test "templates todolists create is out of scope" { + mark_out_of_scope "Account-wide template mutation" +} + +@test "todolists templatify is out of scope" { + mark_out_of_scope "Writes a new template into the account library" +} + +@test "todolists templatification is out of scope" { + mark_out_of_scope "Depends on todolists templatify (OOS)" +} + +@test "card-tables templatify is out of scope" { + mark_out_of_scope "Writes a new template into the account library" +} + +@test "card-tables templatification is out of scope" { + mark_out_of_scope "Depends on card-tables templatify (OOS)" +} + @test "messagetypes create is out of scope" { mark_out_of_scope "Account-wide message type mutation" } diff --git a/e2e/templates.bats b/e2e/templates.bats index 435618c6..d344daab 100644 --- a/e2e/templates.bats +++ b/e2e/templates.bats @@ -213,3 +213,175 @@ load test_helper run basecamp templates foobar # Command may show help or require project - just verify it runs } + + +# Kind subgroups + +@test "templates --help lists the kind subgroups" { + run basecamp templates --help + assert_success + assert_output_contains "projects" + assert_output_contains "todolists" +} + +@test "templates projects without subcommand shows help" { + run basecamp templates projects + assert_success + assert_output_contains "construct" + assert_output_contains "construction" +} + +@test "templates todolists without subcommand shows help" { + run basecamp templates todolists + assert_success + assert_output_contains "duplicate" + assert_output_contains "duplication" +} + +@test "templates projects construct without project name shows error" { + create_credentials + create_global_config '{"account_id": 99999}' + + run basecamp templates projects construct 123 + assert_failure + assert_output_contains "name required" +} + +@test "templates projects list --status with invalid value shows error" { + create_credentials + create_global_config '{"account_id": 99999}' + + run basecamp templates projects list --status bogus + assert_failure + assert_output_contains "unknown --status value" +} + +@test "templates todolists duplicate without template id shows error" { + create_credentials + create_global_config '{"account_id": 99999}' + + run basecamp templates todolists duplicate + assert_failure + assert_output_contains "ID required" +} + +@test "templates todolists duplicate accepts --in and --confirm-adding-people" { + run basecamp templates todolists duplicate --help + assert_success + assert_output_contains "--in" + assert_output_contains "--confirm-adding-people" +} + +@test "templates todolists duplication without id shows error" { + create_credentials + create_global_config '{"account_id": 99999}' + + run basecamp templates todolists duplication + assert_failure + assert_output_contains "ID required" +} + +@test "templates todolists copy is an alias for duplicate" { + create_credentials + create_global_config '{"account_id": 99999}' + + run basecamp templates todolists copy + assert_failure + assert_output_contains "ID required" +} + +@test "templates todolists copy-status is an alias for duplication" { + create_credentials + create_global_config '{"account_id": 99999}' + + run basecamp templates todolists copy-status + assert_failure + assert_output_contains "ID required" +} + +@test "templates card-tables without subcommand shows help" { + run basecamp templates card-tables + assert_success + assert_output_contains "duplicate" + assert_output_contains "duplication" +} + +@test "templates card-tables create without name shows error" { + create_credentials + create_global_config '{"account_id": 99999}' + + run basecamp templates card-tables create + assert_failure + assert_json_value '.error' '<name> required' +} + +@test "templates card-tables duplicate without template id shows error" { + create_credentials + create_global_config '{"account_id": 99999}' + + run basecamp templates card-tables duplicate + assert_failure + assert_output_contains "ID required" +} + +@test "templates card-tables duplicate has no --todoset flag" { + run basecamp templates card-tables duplicate --help + assert_success + assert_output_contains "--in" + assert_output_not_contains "--todoset" +} + +@test "templates card-tables duplication without id shows error" { + create_credentials + create_global_config '{"account_id": 99999}' + + run basecamp templates card-tables duplication + assert_failure + assert_output_contains "ID required" +} + +@test "todolists templatify without id shows error" { + create_credentials + create_global_config '{"account_id": 99999}' + + run basecamp todolists templatify + assert_failure + assert_output_contains "ID required" +} + +@test "todolists templatify has no --move-cards-to-triage flag" { + run basecamp todolists templatify --help + assert_success + assert_output_contains "--copy-comments" + assert_output_not_contains "--move-cards-to-triage" +} + +@test "card-tables without subcommand shows help" { + run basecamp card-tables + assert_success + assert_output_contains "templatify" + assert_output_contains "templatification" +} + +@test "card-tables templatify offers --move-cards-to-triage" { + run basecamp card-tables templatify --help + assert_success + assert_output_contains "--move-cards-to-triage" +} + +@test "card-tables templatification requires two ids" { + create_credentials + create_global_config '{"account_id": 99999}' + + run basecamp card-tables templatification 123 + assert_failure +} + +@test "templates todolists create without name shows error" { + create_credentials + create_global_config '{"account_id": 99999}' + + run basecamp templates todolists create + assert_failure + assert_json_value '.error' '<name> required' +} diff --git a/internal/cli/root.go b/internal/cli/root.go index 16301d5f..ff3c3293 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -321,6 +321,7 @@ func Execute() { cmd.AddCommand(commands.NewUnassignCmd()) cmd.AddCommand(commands.NewMessagesCmd()) cmd.AddCommand(commands.NewCardsCmd()) + cmd.AddCommand(commands.NewCardTablesCmd()) cmd.AddCommand(commands.NewURLCmd()) cmd.AddCommand(commands.NewSearchCmd()) cmd.AddCommand(commands.NewRecordingsCmd()) diff --git a/internal/commands/cardtables.go b/internal/commands/cardtables.go new file mode 100644 index 00000000..c60ee441 --- /dev/null +++ b/internal/commands/cardtables.go @@ -0,0 +1,30 @@ +package commands + +import ( + "github.com/spf13/cobra" +) + +// NewCardTablesCmd creates the card-tables command for managing card tables themselves. +func NewCardTablesCmd() *cobra.Command { + var project string + + cmd := &cobra.Command{ + Use: "card-tables", + Short: "Manage card tables", + Long: `Manage card tables in a project. + +A card table is the board that contains columns and cards. Use this group for +actions on the table itself, such as saving it as a reusable template. Use +'basecamp cards' to manage cards, columns, steps, and wormholes inside a table.`, + } + + cmd.PersistentFlags().StringVarP(&project, "project", "p", "", "Project ID or name") + cmd.PersistentFlags().StringVar(&project, "in", "", "Project ID (alias for --project)") + + cmd.AddCommand( + newTemplatifyCmd(cardTableTemplatificationKind(), &project), + newTemplatificationCmd(cardTableTemplatificationKind(), &project), + ) + + return cmd +} diff --git a/internal/commands/commands.go b/internal/commands/commands.go index 61f738d1..d69e2592 100644 --- a/internal/commands/commands.go +++ b/internal/commands/commands.go @@ -36,13 +36,14 @@ func CommandCategories() []CommandCategory { Commands: []CommandInfo{ {Name: "projects", Category: "core", Description: "Manage projects", Actions: []string{"list", "show", "create", "update", "delete"}}, {Name: "todos", Category: "core", Description: "Manage to-dos", Actions: []string{"list", "show", "create", "update", "complete", "uncomplete", "position", "trash", "archive", "restore"}}, - {Name: "todolists", Category: "core", Description: "Manage to-do lists", Actions: []string{"list", "show", "create", "update", "position", "trash", "archive", "restore"}}, + {Name: "todolists", Category: "core", Description: "Manage to-do lists", Actions: []string{"list", "show", "create", "update", "position", "templatify", "templatification", "trash", "archive", "restore"}}, {Name: "todosets", Category: "core", Description: "Manage to-do set containers", Actions: []string{"list", "show"}}, {Name: "hillcharts", Category: "core", Description: "Manage hill charts", Actions: []string{"show", "track", "untrack"}}, {Name: "gauges", Category: "core", Description: "Manage gauges", Actions: []string{"list", "needles", "needle", "create", "update", "delete", "enable", "disable"}}, {Name: "todolistgroups", Category: "core", Description: "Manage to-do list groups", Actions: []string{"list", "show", "create", "update", "position"}}, {Name: "messages", Category: "core", Description: "Manage messages", Actions: []string{"list", "show", "create", "update", "publish", "pin", "unpin", "trash", "archive", "restore"}}, {Name: "chat", Category: "core", Description: "Chat in real-time", Actions: []string{"list", "messages", "post", "upload", "line", "update", "delete"}}, + {Name: "card-tables", Category: "core", Description: "Manage card tables", Actions: []string{"templatify", "templatification"}}, {Name: "cards", Category: "core", Description: "Manage Kanban cards", Actions: []string{"list", "show", "create", "update", "move", "done", "columns", "wormholes", "steps", "trash", "archive", "restore"}}, {Name: "files", Category: "core", Description: "Manage files, documents, and folders", Actions: []string{"list", "show", "versions", "replace", "download", "update", "trash", "archive", "restore"}}, {Name: "checkins", Category: "core", Description: "View automatic check-ins", Actions: []string{"questions", "question", "answers", "answer", "reminders"}}, @@ -89,7 +90,7 @@ func CommandCategories() []CommandCategory { Name: "Organization", Commands: []CommandInfo{ {Name: "people", Category: "organization", Description: "Manage people and access", Actions: []string{"list", "show", "update", "out-of-office", "pingable", "add", "remove", "clients"}}, - {Name: "templates", Category: "organization", Description: "Manage project and to-do list templates", Actions: []string{"list", "show", "create", "update", "delete", "construct", "construction", "library", "copy", "copy-status"}}, + {Name: "templates", Category: "organization", Description: "Manage project, to-do list, and card table templates", Actions: []string{"projects", "todolists", "card-tables"}}, {Name: "webhooks", Category: "organization", Description: "Manage webhooks", Actions: []string{"list", "show", "create", "update", "delete"}}, {Name: "lineup", Category: "organization", Description: "Manage lineup markers", Actions: []string{"list", "create", "update", "delete"}}, }, diff --git a/internal/commands/commands_test.go b/internal/commands/commands_test.go index d295c335..1f4feec2 100644 --- a/internal/commands/commands_test.go +++ b/internal/commands/commands_test.go @@ -69,6 +69,7 @@ func buildRootWithAllCommands() *cobra.Command { root.AddCommand(commands.NewUnassignCmd()) root.AddCommand(commands.NewMessagesCmd()) root.AddCommand(commands.NewCardsCmd()) + root.AddCommand(commands.NewCardTablesCmd()) root.AddCommand(commands.NewURLCmd()) root.AddCommand(commands.NewSearchCmd()) root.AddCommand(commands.NewRecordingsCmd()) diff --git a/internal/commands/templates.go b/internal/commands/templates.go index a8482b3d..9434caca 100644 --- a/internal/commands/templates.go +++ b/internal/commands/templates.go @@ -20,12 +20,36 @@ import ( func NewTemplatesCmd() *cobra.Command { cmd := &cobra.Command{ Use: "templates", - Short: "Manage project and to-do list templates", - Long: `Manage project templates and the account's to-do list template library. + Short: "Manage project, to-do list, and card table templates", + Long: `Manage the account's template library, grouped by kind of template. -Project templates create projects with predefined structure, tools, and content. -Library templates copy a reusable to-do list into an existing project.`, - Annotations: map[string]string{"agent_notes": "Project construction and to-do list template copies are asynchronous. Poll construction or copy-status until status=completed. construct --start-date anchors the template's relative dates to the Sunday of that week; without it they anchor to the week of construction. Copy grants referenced people project access only with --confirm-adding-people."}, + projects Project templates: construct a whole project from a blueprint. + todolists To-do list templates: duplicate a reusable list into a project. + card-tables Card table templates: duplicate a reusable board into a project. + +The pre-grouping spellings (templates list, templates library, templates copy, +and the rest) still work and resolve to the same commands.`, + Annotations: map[string]string{"agent_notes": "Project construction and to-do list or card table template duplication are asynchronous. Poll templates projects construction or templates <kind> duplication until status=completed. construct --start-date anchors the template's relative dates to the Sunday of that week; without it they anchor to the week of construction. Duplicating grants referenced people project access only with --confirm-adding-people."}, + } + + cmd.AddCommand( + newTemplatesProjectsCmd(), + newTemplatesTodolistsCmd(), + newTemplatesCardTablesCmd(), + ) + cmd.AddCommand(templatesBackCompatCmds()...) + + return cmd +} + +func newTemplatesProjectsCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "projects", + Short: "Manage project templates", + Long: `Manage project templates. + +A project template is a blueprint for a whole project: its tools, structure, +and starting content. Constructing one creates a new project.`, } cmd.AddCommand( @@ -36,14 +60,311 @@ Library templates copy a reusable to-do list into an existing project.`, newTemplatesDeleteCmd(), newTemplatesConstructCmd(), newTemplatesConstructionCmd(), - newTemplatesLibraryCmd(), - newTemplatesCopyCmd(), - newTemplatesCopyStatusCmd(), ) return cmd } +func newTemplatesTodolistsCmd() *cobra.Command { + kind := todolistTemplateKind() + + cmd := &cobra.Command{ + Use: "todolists", + Short: "Manage to-do list templates", + Long: `Manage the account's to-do list template library. + +A to-do list template is a reusable list. Duplicating one adds it to an +existing project's To-dos tool.`, + } + + cmd.AddCommand( + newTemplatesLibraryListCmd("list"), + newTemplatesTodolistsCreateCmd(), + newTemplatesDuplicateCmd(kind, "duplicate <template_id>", "copy"), + newTemplatesDuplicationCmd(kind, "duplication <duplication_id>", "copy-status"), + ) + + return cmd +} + +func templatesBackCompatCmds() []*cobra.Command { + kind := todolistTemplateKind() + + // Registered a second time rather than aliased: cobra aliases rename a + // command where it sits, and these moved a level down the tree. + return []*cobra.Command{ + newTemplatesListCmd(), + newTemplatesShowCmd(), + newTemplatesCreateCmd(), + newTemplatesUpdateCmd(), + newTemplatesDeleteCmd(), + newTemplatesConstructCmd(), + newTemplatesConstructionCmd(), + newTemplatesLibraryListCmd("library"), + newTemplatesDuplicateCmd(kind, "copy <template_id>"), + newTemplatesDuplicationCmd(kind, "copy-status <copy_id>"), + } +} + +func newTemplatesCardTablesCmd() *cobra.Command { + kind := cardTableTemplateKind() + + cmd := &cobra.Command{ + Use: "card-tables", + Short: "Manage card table templates", + Long: `Manage the account's card table template library. + +A card table template is a reusable board with its columns. Duplicating one +adds it to an existing project.`, + } + + cmd.AddCommand( + newTemplatesCardTablesListCmd(), + newTemplatesCardTablesCreateCmd(), + newTemplatesDuplicateCmd(kind, "duplicate <template_id>", "copy"), + newTemplatesDuplicationCmd(kind, "duplication <duplication_id>", "copy-status"), + ) + + return cmd +} + +func newTemplatesCardTablesListCmd() *cobra.Command { + return &cobra.Command{ + Use: "list", + Short: "List card table templates", + Long: "List the account's active card table templates.", + RunE: func(cmd *cobra.Command, args []string) error { + app := appctx.FromContext(cmd.Context()) + persistentAccount := hasPersistentAccount(app.Config) + if err := ensureAccount(cmd, app); err != nil { + return err + } + contextArgs := templateCommandContextArgs( + app.Config.ActiveProfile, + persistentAccount, + app.Config.AccountID, + ) + + library, err := app.Account().Templates().GetLibraryCardTables(cmd.Context()) + if err != nil { + return convertSDKError(err) + } + + display := make([]struct { + ID int64 `json:"id"` + Title string `json:"title"` + }, len(library.CardTables)) + for i, cardTable := range library.CardTables { + display[i].ID = cardTable.ID + display[i].Title = cardTable.Title + } + + return app.OK(library, + output.WithDisplayData(display), + output.WithSummary(fmt.Sprintf("%d active card table templates", len(library.CardTables))), + output.WithBreadcrumbs( + output.Breadcrumb{ + Action: "duplicate", + Cmd: "basecamp templates card-tables duplicate <template-id> --in <project>" + contextArgs, + Description: "Duplicate a template into a project", + }, + ), + ) + }, + } +} + +func newTemplatesTodolistsCreateCmd() *cobra.Command { + var name string + var description string + + cmd := &cobra.Command{ + Use: "create <name>", + Short: "Create an empty to-do list template", + Long: "Create an empty to-do list template in the account's library.", + Args: cobra.MaximumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) > 0 && name == "" { + name = args[0] + } + if name == "" { + return missingArg(cmd, "<name>") + } + + description, err := resolveContentValue(cmd, description, -1, "--description") + if err != nil { + return err + } + + app := appctx.FromContext(cmd.Context()) + persistentAccount := hasPersistentAccount(app.Config) + if err := ensureAccount(cmd, app); err != nil { + return err + } + contextArgs := templateCommandContextArgs( + app.Config.ActiveProfile, + persistentAccount, + app.Config.AccountID, + ) + + todolist, err := app.Account().Templates().CreateLibraryTodolist(cmd.Context(), &basecamp.CreateTemplateLibraryTodolistRequest{ + Name: name, + Description: description, + }) + if err != nil { + return convertSDKError(err) + } + + return app.OK(todolist, + output.WithSummary(fmt.Sprintf("Created to-do list template #%d: %s", todolist.ID, todolist.Name)), + output.WithBreadcrumbs( + output.Breadcrumb{ + Action: "duplicate", + Cmd: fmt.Sprintf("basecamp templates todolists duplicate %d --in <project>%s", todolist.ID, contextArgs), + Description: "Duplicate the template into a project", + }, + ), + ) + }, + } + + cmd.Flags().StringVar(&name, "name", "", "Template name") + cmd.Flags().StringVar(&description, "description", "", "Template description; use - to read from stdin") + cmd.Flags().StringVar(&description, "desc", "", "Template description (alias)") + + allowDash(cmd, "flag:description", "flag:desc") + + return cmd +} + +func newTemplatesCardTablesCreateCmd() *cobra.Command { + var name string + + cmd := &cobra.Command{ + Use: "create <name>", + Short: "Create an empty card table template", + Long: "Create an empty card table template in the account's library.", + Args: cobra.MaximumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) > 0 && name == "" { + name = args[0] + } + if name == "" { + return missingArg(cmd, "<name>") + } + + app := appctx.FromContext(cmd.Context()) + persistentAccount := hasPersistentAccount(app.Config) + if err := ensureAccount(cmd, app); err != nil { + return err + } + contextArgs := templateCommandContextArgs( + app.Config.ActiveProfile, + persistentAccount, + app.Config.AccountID, + ) + + cardTable, err := app.Account().Templates().CreateLibraryCardTable(cmd.Context(), name) + if err != nil { + return convertSDKError(err) + } + + return app.OK(cardTable, + output.WithSummary(fmt.Sprintf("Created card table template #%d: %s", cardTable.ID, cardTable.Title)), + output.WithBreadcrumbs( + output.Breadcrumb{ + Action: "duplicate", + Cmd: fmt.Sprintf("basecamp templates card-tables duplicate %d --in <project>%s", cardTable.ID, contextArgs), + Description: "Duplicate the template into a project", + }, + ), + ) + }, + } + + cmd.Flags().StringVar(&name, "name", "", "Template name") + + return cmd +} + +// What differs by kind when duplicating: how a pinned container is checked, and +// what the finished copy points at. Basecamp resolves the container itself from +// the destination project, so the unpinned path is the same for every kind. +type templateCopyKind struct { + noun string + group string + containerFlag string + containerUsage string + resolveContainer func(cmd *cobra.Command, app *appctx.App, projectID, container string) (int64, error) + describeCompletedResult func(templateCopy *basecamp.TemplateLibraryCopy, contextArgs string) (string, []output.Breadcrumb, bool) +} + +func todolistTemplateKind() templateCopyKind { + return templateCopyKind{ + noun: "to-do list", + group: "todolists", + containerFlag: "todoset", + containerUsage: "To-dos tool ID (auto-detected from project)", + resolveContainer: resolveTemplateCopyTodoset, + describeCompletedResult: describeTodolistDuplication, + } +} + +func cardTableTemplateKind() templateCopyKind { + return templateCopyKind{ + noun: "card table", + group: "card-tables", + describeCompletedResult: describeCardTableDuplication, + } +} + +func describeCardTableDuplication(templateCopy *basecamp.TemplateLibraryCopy, contextArgs string) (string, []output.Breadcrumb, bool) { + table := templateCopy.DestinationCardTable + if table == nil { + return "", nil, false + } + + breadcrumbs := []output.Breadcrumb{ + { + Action: "cards", + Cmd: fmt.Sprintf("basecamp cards list --card-table %d%s", table.ID, contextArgs), + Description: "List cards on the duplicated board", + }, + } + + return fmt.Sprintf("Template duplication complete: %s (card table #%d)", table.Title, table.ID), breadcrumbs, true +} + +func describeTodolistDuplication(templateCopy *basecamp.TemplateLibraryCopy, contextArgs string) (string, []output.Breadcrumb, bool) { + list := templateCopy.DestinationTodolist + if list == nil { + return "", nil, false + } + + breadcrumbs := []output.Breadcrumb{ + { + Action: "show", + Cmd: fmt.Sprintf("basecamp todolists show %d --in %d%s", list.ID, list.Bucket.ID, contextArgs), + Description: "View the duplicated to-do list", + }, + } + + return fmt.Sprintf("Template duplication complete: %s (to-do list #%d)", list.Name, list.ID), breadcrumbs, true +} + +func resolveTemplateCopyTodoset(cmd *cobra.Command, app *appctx.App, projectID, todoset string) (int64, error) { + if err := validateTemplateCopyTodoset(cmd, app, todoset, projectID); err != nil { + return 0, err + } + + todosetID, err := strconv.ParseInt(todoset, 10, 64) + if err != nil { + return 0, output.ErrUsage("Invalid todoset ID") + } + + return todosetID, nil +} + func newTemplatesListCmd() *cobra.Command { var status string @@ -121,26 +442,26 @@ func runTemplatesList(cmd *cobra.Command, status string) error { output.WithBreadcrumbs( output.Breadcrumb{ Action: "show", - Cmd: "basecamp templates show <id>", + Cmd: "basecamp templates projects show <id>", Description: "View template details", }, output.Breadcrumb{ Action: "create", - Cmd: "basecamp templates create \"Name\"", + Cmd: "basecamp templates projects create \"Name\"", Description: "Create new template", }, output.Breadcrumb{ Action: "construct", - Cmd: "basecamp templates construct <id> --name \"Project Name\"", + Cmd: "basecamp templates projects construct <id> --name \"Project Name\"", Description: "Create project from template", }, ), ) } -func newTemplatesLibraryCmd() *cobra.Command { +func newTemplatesLibraryListCmd(use string) *cobra.Command { return &cobra.Command{ - Use: "library", + Use: use, Short: "List to-do list templates", Long: "List the account's active to-do list templates.", RunE: func(cmd *cobra.Command, args []string) error { @@ -155,7 +476,7 @@ func newTemplatesLibraryCmd() *cobra.Command { app.Config.AccountID, ) - library, err := app.Account().Templates().GetLibrary(cmd.Context()) + library, err := app.Account().Templates().GetLibraryTodolists(cmd.Context()) if err != nil { return convertSDKError(err) } @@ -174,9 +495,9 @@ func newTemplatesLibraryCmd() *cobra.Command { output.WithSummary(fmt.Sprintf("%d active to-do list templates", len(library.Todolists))), output.WithBreadcrumbs( output.Breadcrumb{ - Action: "copy", - Cmd: "basecamp templates copy <template-id> --in <project>" + contextArgs, - Description: "Copy a template into a project", + Action: "duplicate", + Cmd: "basecamp templates todolists duplicate <template-id> --in <project>" + contextArgs, + Description: "Duplicate a template into a project", }, ), ) @@ -184,27 +505,30 @@ func newTemplatesLibraryCmd() *cobra.Command { } } -func newTemplatesCopyCmd() *cobra.Command { +func newTemplatesDuplicateCmd(kind templateCopyKind, use string, aliases ...string) *cobra.Command { var project string - var todoset string + var container string var confirmAddingPeople bool cmd := &cobra.Command{ - Use: "copy <template_id>", - Short: "Copy a to-do list template into a project", - Long: `Start copying a to-do list template into a project's To-dos tool. - -The copy runs asynchronously. Use 'templates copy-status' with the returned -copy ID to check its progress. Referenced people receive project access only -when --confirm-adding-people is explicitly provided.`, + Use: use, + Aliases: aliases, + Short: "Duplicate a " + kind.noun + " template into a project", + Long: fmt.Sprintf(`Start duplicating a %s template into a project. + +The duplication runs asynchronously. Use 'templates %s duplication' with the +returned ID to check its progress. Referenced people receive project access +only when --confirm-adding-people is explicitly provided.`, kind.noun, kind.group), Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { templateID, err := strconv.ParseInt(args[0], 10, 64) if err != nil { return output.ErrUsage("Invalid template ID") } - if err := requireNumericID(todoset, "todoset ID"); err != nil { - return err + if kind.containerFlag != "" { + if err := requireNumericID(container, kind.containerFlag+" ID"); err != nil { + return err + } } app := appctx.FromContext(cmd.Context()) @@ -222,45 +546,47 @@ when --confirm-adding-people is explicitly provided.`, if err != nil { return err } - if todoset != "" { - if err := validateTemplateCopyTodoset(cmd, app, todoset, resolvedProjectID); err != nil { + + req := &basecamp.CreateTemplateLibraryCopyRequest{ + TemplateRecordingID: templateID, + AddingPeopleConfirmed: confirmAddingPeople, + } + if container == "" { + projectID, err := strconv.ParseInt(resolvedProjectID, 10, 64) + if err != nil { + return output.ErrUsage("Invalid project ID") + } + req.DestinationProjectID = projectID + } else { + req.DestinationParentID, err = kind.resolveContainer(cmd, app, resolvedProjectID, container) + if err != nil { return err } } - resolvedTodosetID, err := ensureTodoset(cmd, app, resolvedProjectID, todoset) - if err != nil { - return err - } - destinationParentID, err := strconv.ParseInt(resolvedTodosetID, 10, 64) - if err != nil { - return output.ErrUsage("Invalid todoset ID") - } - templateCopy, err := app.Account().Templates().CreateLibraryCopy(cmd.Context(), &basecamp.CreateTemplateLibraryCopyRequest{ - TemplateRecordingID: templateID, - DestinationParentID: destinationParentID, - AddingPeopleConfirmed: confirmAddingPeople, - }) + templateCopy, err := app.Account().Templates().CreateLibraryCopy(cmd.Context(), req) if err != nil { - return templateCopyError(err, templateID, resolvedProjectID, resolvedTodosetID, contextArgs) + return templateCopyError(err, kind, templateID, resolvedProjectID, container, contextArgs) } return app.OK(templateCopy, - output.WithSummary(fmt.Sprintf("Started template copy #%d (%s)", templateCopy.ID, templateCopy.Status)), + output.WithSummary(fmt.Sprintf("Started template duplication #%d (%s)", templateCopy.ID, templateCopy.Status)), output.WithBreadcrumbs( output.Breadcrumb{ Action: "status", - Cmd: fmt.Sprintf("basecamp templates copy-status %d%s", templateCopy.ID, contextArgs), - Description: "Check copy status", + Cmd: fmt.Sprintf("basecamp templates %s duplication %d%s", kind.group, templateCopy.ID, contextArgs), + Description: "Check duplication status", }, ), ) }, } - cmd.Flags().StringVarP(&project, "project", "p", "", "Project ID or name") - cmd.Flags().StringVar(&project, "in", "", "Project ID or name (alias for --project)") - cmd.Flags().StringVar(&todoset, "todoset", "", "To-dos tool ID (auto-detected from project)") + cmd.Flags().StringVarP(&project, "project", "p", "", "Destination project ID or name") + cmd.Flags().StringVar(&project, "in", "", "Destination project ID or name (alias for --project)") + if kind.containerFlag != "" { + cmd.Flags().StringVar(&container, kind.containerFlag, "", kind.containerUsage) + } cmd.Flags().BoolVar(&confirmAddingPeople, "confirm-adding-people", false, "Grant referenced people access to the destination project") return cmd @@ -300,7 +626,7 @@ func templateCommandContextArgs(profile string, persistentAccount bool, accountI return args + replyAccountArg(persistentAccount, accountID) } -func templateCopyError(err error, templateID int64, projectID, todosetID, contextArgs string) error { +func templateCopyError(err error, kind templateCopyKind, templateID int64, projectID, container, contextArgs string) error { var confirmationErr *basecamp.PeopleConfirmationRequiredError if !errors.As(err, &confirmationErr) { return convertSDKError(err) @@ -311,7 +637,12 @@ func templateCopyError(err error, templateID int64, projectID, todosetID, contex people = append(people, fmt.Sprintf("%s (#%d)", person.Name, person.ID)) } - rerun := fmt.Sprintf("basecamp templates copy %d --in %s --todoset %s%s --confirm-adding-people", templateID, projectID, todosetID, contextArgs) + pinned := "" + if container != "" { + pinned = fmt.Sprintf(" --%s %s", kind.containerFlag, container) + } + rerun := fmt.Sprintf("basecamp templates %s duplicate %d --in %s%s%s --confirm-adding-people", + kind.group, templateID, projectID, pinned, contextArgs) converted := output.AsError(err) return &output.Error{ @@ -324,16 +655,17 @@ func templateCopyError(err error, templateID int64, projectID, todosetID, contex } } -func newTemplatesCopyStatusCmd() *cobra.Command { +func newTemplatesDuplicationCmd(kind templateCopyKind, use string, aliases ...string) *cobra.Command { return &cobra.Command{ - Use: "copy-status <copy_id>", - Short: "Check a to-do list template copy", - Long: "Check whether a to-do list template copy is pending, processing, completed, or failed.", - Args: cobra.ExactArgs(1), + Use: use, + Aliases: aliases, + Short: "Check a " + kind.noun + " template duplication", + Long: "Check whether a " + kind.noun + " template duplication is pending, processing, completed, or failed.", + Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { copyID, err := strconv.ParseInt(args[0], 10, 64) if err != nil { - return output.ErrUsage("Invalid copy ID") + return output.ErrUsage("Invalid duplication ID") } app := appctx.FromContext(cmd.Context()) @@ -352,7 +684,7 @@ func newTemplatesCopyStatusCmd() *cobra.Command { return convertSDKError(err) } - summary, breadcrumbs := templateCopyStatusOutput(templateCopy, contextArgs) + summary, breadcrumbs := templateCopyStatusOutput(templateCopy, kind, contextArgs) return app.OK(templateCopy, output.WithSummary(summary), output.WithBreadcrumbs(breadcrumbs...), @@ -361,39 +693,31 @@ func newTemplatesCopyStatusCmd() *cobra.Command { } } -func templateCopyStatusOutput(templateCopy *basecamp.TemplateLibraryCopy, contextArgs string) (string, []output.Breadcrumb) { +func templateCopyStatusOutput(templateCopy *basecamp.TemplateLibraryCopy, kind templateCopyKind, contextArgs string) (string, []output.Breadcrumb) { switch templateCopy.Status { case "completed": - if templateCopy.DestinationTodolist != nil { - list := templateCopy.DestinationTodolist - breadcrumbs := []output.Breadcrumb{ - { - Action: "show", - Cmd: fmt.Sprintf("basecamp todolists show %d --in %d%s", list.ID, list.Bucket.ID, contextArgs), - Description: "View copied to-do list", - }, - } - return fmt.Sprintf("Template copy complete: %s (to-do list #%d)", list.Name, list.ID), breadcrumbs + if summary, breadcrumbs, ok := kind.describeCompletedResult(templateCopy, contextArgs); ok { + return summary, breadcrumbs } - return fmt.Sprintf("Template copy #%d completed", templateCopy.ID), nil + return fmt.Sprintf("Template duplication #%d completed", templateCopy.ID), nil case "failed": - return fmt.Sprintf("Template copy #%d failed", templateCopy.ID), []output.Breadcrumb{ + return fmt.Sprintf("Template duplication #%d failed", templateCopy.ID), []output.Breadcrumb{ { - Action: "library", - Cmd: "basecamp templates library" + contextArgs, + Action: "list", + Cmd: fmt.Sprintf("basecamp templates %s list%s", kind.group, contextArgs), Description: "List available templates", }, } case "pending", "processing": - return fmt.Sprintf("Template copy #%d is %s", templateCopy.ID, templateCopy.Status), []output.Breadcrumb{ + return fmt.Sprintf("Template duplication #%d is %s", templateCopy.ID, templateCopy.Status), []output.Breadcrumb{ { Action: "poll", - Cmd: fmt.Sprintf("basecamp templates copy-status %d%s", templateCopy.ID, contextArgs), + Cmd: fmt.Sprintf("basecamp templates %s duplication %d%s", kind.group, templateCopy.ID, contextArgs), Description: "Check again", }, } default: - return fmt.Sprintf("Template copy #%d status: %s", templateCopy.ID, templateCopy.Status), nil + return fmt.Sprintf("Template duplication #%d status: %s", templateCopy.ID, templateCopy.Status), nil } } @@ -425,17 +749,17 @@ func newTemplatesShowCmd() *cobra.Command { output.WithBreadcrumbs( output.Breadcrumb{ Action: "construct", - Cmd: fmt.Sprintf("basecamp templates construct %d --name \"Project Name\"", templateID), + Cmd: fmt.Sprintf("basecamp templates projects construct %d --name \"Project Name\"", templateID), Description: "Create project from template", }, output.Breadcrumb{ Action: "update", - Cmd: fmt.Sprintf("basecamp templates update %d --name \"New Name\"", templateID), + Cmd: fmt.Sprintf("basecamp templates projects update %d --name \"New Name\"", templateID), Description: "Update template", }, output.Breadcrumb{ Action: "list", - Cmd: "basecamp templates", + Cmd: "basecamp templates projects list", Description: "List all templates", }, ), @@ -490,12 +814,12 @@ func newTemplatesCreateCmd() *cobra.Command { output.WithBreadcrumbs( output.Breadcrumb{ Action: "show", - Cmd: fmt.Sprintf("basecamp templates show %d", template.ID), + Cmd: fmt.Sprintf("basecamp templates projects show %d", template.ID), Description: "View template", }, output.Breadcrumb{ Action: "construct", - Cmd: fmt.Sprintf("basecamp templates construct %d --name \"Project Name\"", template.ID), + Cmd: fmt.Sprintf("basecamp templates projects construct %d --name \"Project Name\"", template.ID), Description: "Create project from template", }, ), @@ -568,7 +892,7 @@ func newTemplatesUpdateCmd() *cobra.Command { output.WithBreadcrumbs( output.Breadcrumb{ Action: "show", - Cmd: fmt.Sprintf("basecamp templates show %d", templateID), + Cmd: fmt.Sprintf("basecamp templates projects show %d", templateID), Description: "View template", }, ), @@ -613,12 +937,12 @@ func newTemplatesDeleteCmd() *cobra.Command { output.WithBreadcrumbs( output.Breadcrumb{ Action: "list", - Cmd: "basecamp templates", + Cmd: "basecamp templates projects list", Description: "List templates", }, output.Breadcrumb{ Action: "trashed", - Cmd: "basecamp templates --status trashed", + Cmd: "basecamp templates projects list --status trashed", Description: "View trashed templates", }, ), @@ -685,7 +1009,7 @@ the given date; without it they anchor to the week the project is constructed.`, output.WithBreadcrumbs( output.Breadcrumb{ Action: "status", - Cmd: fmt.Sprintf("basecamp templates construction %d %d", templateID, construction.ID), + Cmd: fmt.Sprintf("basecamp templates projects construction %d %d", templateID, construction.ID), Description: "Check construction status", }, ), @@ -752,7 +1076,7 @@ the response includes the newly created project.`, breadcrumbs = []output.Breadcrumb{ { Action: "poll", - Cmd: fmt.Sprintf("basecamp templates construction %d %d", templateID, constructionID), + Cmd: fmt.Sprintf("basecamp templates projects construction %d %d", templateID, constructionID), Description: "Check again", }, } @@ -765,3 +1089,210 @@ the response includes the newly created project.`, }, } } + +// What differs by kind when saving work as a template: what a finished save +// points at, and whether the board-only triage flag is offered. bc3 accepts +// move_cards_to_triage on any recording but never reads it for a to-do list, +// so only the card table command registers it. +type templatificationKind struct { + noun string + group string + offersTriage bool + describeCompletedResult func(save *basecamp.Templatification, contextArgs string) (string, []output.Breadcrumb, bool) +} + +func todolistTemplatificationKind() templatificationKind { + return templatificationKind{ + noun: "to-do list", + group: "todolists", + describeCompletedResult: func(save *basecamp.Templatification, contextArgs string) (string, []output.Breadcrumb, bool) { + list := save.DestinationTodolist + if list == nil { + return "", nil, false + } + return fmt.Sprintf("Saved as template: %s (to-do list template #%d)", list.Name, list.ID), + []output.Breadcrumb{ + { + Action: "list", + Cmd: "basecamp templates todolists list" + contextArgs, + Description: "List to-do list templates", + }, + }, true + }, + } +} + +func cardTableTemplatificationKind() templatificationKind { + return templatificationKind{ + noun: "card table", + group: "card-tables", + offersTriage: true, + describeCompletedResult: func(save *basecamp.Templatification, contextArgs string) (string, []output.Breadcrumb, bool) { + table := save.DestinationCardTable + if table == nil { + return "", nil, false + } + return fmt.Sprintf("Saved as template: %s (card table template #%d)", table.Title, table.ID), + []output.Breadcrumb{ + { + Action: "list", + Cmd: "basecamp templates card-tables list" + contextArgs, + Description: "List card table templates", + }, + }, true + }, + } +} + +func newTemplatifyCmd(kind templatificationKind, project *string) *cobra.Command { + var name string + var copyComments bool + var copyAssignments bool + var moveCardsToTriage bool + + cmd := &cobra.Command{ + Use: "templatify <id>", + Short: "Save a " + kind.noun + " as a reusable template", + Long: fmt.Sprintf(`Save an existing %s into the account's template library. + +The save runs asynchronously. Use '%s templatification' with the returned ID +to check its progress. Without --name the template takes the source's title.`, kind.noun, kind.group), + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + recordingID, err := strconv.ParseInt(args[0], 10, 64) + if err != nil { + return output.ErrUsage("Invalid " + kind.noun + " ID") + } + + app := appctx.FromContext(cmd.Context()) + persistentAccount := hasPersistentAccount(app.Config) + if err := ensureAccount(cmd, app); err != nil { + return err + } + contextArgs := templateCommandContextArgs( + app.Config.ActiveProfile, + persistentAccount, + app.Config.AccountID, + ) + + bucketID, err := resolveBucketID(cmd, app, *project) + if err != nil { + return err + } + + save, err := app.Account().Templates().CreateTemplatification(cmd.Context(), bucketID, recordingID, + &basecamp.CreateTemplatificationRequest{ + TemplateName: name, + CopyComments: copyComments, + CopyAssignments: copyAssignments, + MoveCardsToTriage: moveCardsToTriage, + }) + if err != nil { + return convertSDKError(err) + } + + return app.OK(save, + output.WithSummary(fmt.Sprintf("Started saving %s #%d as a template (%s)", kind.noun, recordingID, save.Status)), + output.WithBreadcrumbs( + output.Breadcrumb{ + Action: "status", + Cmd: fmt.Sprintf("basecamp %s templatification %d %d --in %d%s", kind.group, recordingID, save.ID, bucketID, contextArgs), + Description: "Check templatification status", + }, + ), + ) + }, + } + + cmd.Flags().StringVar(&name, "name", "", "Template name (defaults to the source's title)") + cmd.Flags().BoolVar(©Comments, "copy-comments", false, "Carry the comments across") + cmd.Flags().BoolVar(©Assignments, "copy-assignments", false, "Carry assignees and the people involved across") + if kind.offersTriage { + cmd.Flags().BoolVar(&moveCardsToTriage, "move-cards-to-triage", false, "Gather the cards into Triage instead of leaving them where they sit") + } + + return cmd +} + +func newTemplatificationCmd(kind templatificationKind, project *string) *cobra.Command { + return &cobra.Command{ + Use: "templatification <id> <templatification_id>", + Short: "Check a " + kind.noun + " templatification", + Long: "Check a templatification: the record of saving a " + kind.noun + " as a\n" + + "template. Reports pending, processing, completed, or failed.", + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + recordingID, err := strconv.ParseInt(args[0], 10, 64) + if err != nil { + return output.ErrUsage("Invalid " + kind.noun + " ID") + } + templatificationID, err := strconv.ParseInt(args[1], 10, 64) + if err != nil { + return output.ErrUsage("Invalid templatification ID") + } + + app := appctx.FromContext(cmd.Context()) + persistentAccount := hasPersistentAccount(app.Config) + if err := ensureAccount(cmd, app); err != nil { + return err + } + contextArgs := templateCommandContextArgs( + app.Config.ActiveProfile, + persistentAccount, + app.Config.AccountID, + ) + + bucketID, err := resolveBucketID(cmd, app, *project) + if err != nil { + return err + } + + save, err := app.Account().Templates().GetTemplatification(cmd.Context(), bucketID, recordingID, templatificationID) + if err != nil { + return convertSDKError(err) + } + + summary, breadcrumbs := templatificationStatusOutput(save, kind, recordingID, bucketID, contextArgs) + return app.OK(save, + output.WithSummary(summary), + output.WithBreadcrumbs(breadcrumbs...), + ) + }, + } +} + +func templatificationStatusOutput(save *basecamp.Templatification, kind templatificationKind, recordingID, bucketID int64, contextArgs string) (string, []output.Breadcrumb) { + switch save.Status { + case "completed": + if summary, breadcrumbs, ok := kind.describeCompletedResult(save, contextArgs); ok { + return summary, breadcrumbs + } + return fmt.Sprintf("Templatification #%d completed", save.ID), nil + case "failed": + return fmt.Sprintf("Templatification #%d failed", save.ID), nil + case "pending", "processing": + return fmt.Sprintf("Templatification #%d is %s", save.ID, save.Status), []output.Breadcrumb{ + { + Action: "poll", + Cmd: fmt.Sprintf("basecamp %s templatification %d %d --in %d%s", kind.group, recordingID, save.ID, bucketID, contextArgs), + Description: "Check again", + }, + } + default: + return fmt.Sprintf("Templatification #%d status: %s", save.ID, save.Status), nil + } +} + +func resolveBucketID(cmd *cobra.Command, app *appctx.App, project string) (int64, error) { + resolved, err := resolveProjectID(cmd, app, project) + if err != nil { + return 0, err + } + + bucketID, err := strconv.ParseInt(resolved, 10, 64) + if err != nil { + return 0, output.ErrUsage("Invalid project ID") + } + + return bucketID, nil +} diff --git a/internal/commands/templates_test.go b/internal/commands/templates_test.go index 397e11e5..18dcc043 100644 --- a/internal/commands/templates_test.go +++ b/internal/commands/templates_test.go @@ -65,10 +65,10 @@ func decodeTemplateEnvelope(t *testing.T, buf *bytes.Buffer) struct { return envelope } -func TestTemplatesLibraryUsesSDKLibraryEndpoint(t *testing.T) { +func TestTemplatesLibraryUsesCanonicalTodolistsEndpoint(t *testing.T) { app, transport := setupRecordingTestApp(t, stubRoute{ method: http.MethodGet, - path: "/99999/template_library.json", + path: "/99999/template_library/todolists.json", status: http.StatusOK, body: templateLibraryJSON, }) @@ -81,17 +81,17 @@ func TestTemplatesLibraryUsesSDKLibraryEndpoint(t *testing.T) { request := transport.last(t) assert.Equal(t, http.MethodGet, request.Method) - assert.Equal(t, "/99999/template_library.json", request.Path) + assert.Equal(t, "/99999/template_library/todolists.json", request.Path) envelope := decodeTemplateEnvelope(t, buf) assert.Equal(t, "1 active to-do list templates", envelope.Summary) require.Len(t, envelope.Breadcrumbs, 1) - assert.Equal(t, "basecamp templates copy <template-id> --in <project> --profile 'work profile' --account 99999", envelope.Breadcrumbs[0].Cmd) + assert.Equal(t, "basecamp templates todolists duplicate <template-id> --in <project> --profile 'work profile' --account 99999", envelope.Breadcrumbs[0].Cmd) } func TestTemplatesLibraryHumanOutputShowsCopyableID(t *testing.T) { app, _ := setupRecordingTestApp(t, stubRoute{ method: http.MethodGet, - path: "/99999/template_library.json", + path: "/99999/template_library/todolists.json", status: http.StatusOK, body: templateLibraryJSON, }) @@ -105,10 +105,9 @@ func TestTemplatesLibraryHumanOutputShowsCopyableID(t *testing.T) { assert.Contains(t, buf.String(), "3") } -func TestTemplatesCopyResolvesProjectAndTodosTool(t *testing.T) { +func TestTemplatesCopyNamesTheProjectAndLetsBasecampPickTheContainer(t *testing.T) { app, transport := setupRecordingTestApp(t, projectsRoute(), - templateCopyProjectRoute(), stubRoute{ method: http.MethodPost, path: "/99999/template_library/copies.json", @@ -124,17 +123,16 @@ func TestTemplatesCopyResolvesProjectAndTodosTool(t *testing.T) { require.NoError(t, err) requests := transport.recorded() - require.Len(t, requests, 3) + require.Len(t, requests, 2, "no container lookup: Basecamp resolves it from the project") assert.Equal(t, "/99999/projects.json", requests[0].Path) - assert.Equal(t, "/99999/projects/123.json", requests[1].Path) - assert.Equal(t, http.MethodPost, requests[2].Method) - assert.Equal(t, "/99999/template_library/copies.json", requests[2].Path) - assert.JSONEq(t, `{"template_recording_id":3,"destination_parent_id":9}`, requests[2].Body) + assert.Equal(t, http.MethodPost, requests[1].Method) + assert.Equal(t, "/99999/template_library/copies.json", requests[1].Path) + assert.JSONEq(t, `{"template_recording_id":3,"destination_project_id":123}`, requests[1].Body) envelope := decodeTemplateEnvelope(t, buf) - assert.Equal(t, "Started template copy #5 (pending)", envelope.Summary) + assert.Equal(t, "Started template duplication #5 (pending)", envelope.Summary) require.Len(t, envelope.Breadcrumbs, 1) - assert.Equal(t, "basecamp templates copy-status 5 --profile 'work profile' --account 99999", envelope.Breadcrumbs[0].Cmd) + assert.Equal(t, "basecamp templates todolists duplication 5 --profile 'work profile' --account 99999", envelope.Breadcrumbs[0].Cmd) } func TestTemplatesCopySendsExplicitPeopleConfirmation(t *testing.T) { @@ -223,7 +221,7 @@ func TestTemplatesCopyExplainsPeopleConfirmation(t *testing.T) { assert.Equal(t, output.CodeValidation, outputErr.Code) assert.Contains(t, outputErr.Message, "Victor (#4)") assert.Contains(t, outputErr.Message, "Georgia (#7)") - assert.Contains(t, outputErr.Hint, "basecamp templates copy 3 --in 123 --todoset 9 --profile 'work profile; echo unsafe' --account 99999 --confirm-adding-people") + assert.Contains(t, outputErr.Hint, "basecamp templates todolists duplicate 3 --in 123 --profile 'work profile; echo unsafe' --account 99999 --confirm-adding-people") var confirmationErr *basecamp.PeopleConfirmationRequiredError assert.True(t, errors.As(err, &confirmationErr), "typed SDK error should remain available") @@ -256,7 +254,7 @@ func TestTemplatesConstructSendsStartDateUnderProjectEnvelope(t *testing.T) { envelope := decodeTemplateEnvelope(t, buf) assert.Equal(t, "Started project construction #598194962 (pending)", envelope.Summary) require.Len(t, envelope.Breadcrumbs, 1) - assert.Equal(t, "basecamp templates construction 2085958507 598194962", envelope.Breadcrumbs[0].Cmd) + assert.Equal(t, "basecamp templates projects construction 2085958507 598194962", envelope.Breadcrumbs[0].Cmd) } func TestTemplatesConstructOmitsStartDateWhenUnset(t *testing.T) { @@ -338,10 +336,10 @@ func TestTemplatesCopyStatusStates(t *testing.T) { summary string breadcrumb string }{ - {name: "pending", body: templateCopyPendingJSON, summary: "Template copy #5 is pending", breadcrumb: "basecamp templates copy-status 5"}, - {name: "processing", body: `{"id":5,"status":"processing","source_recording_id":3,"destination_parent_id":9,"url":"https://example.test/copies/5.json"}`, summary: "Template copy #5 is processing", breadcrumb: "basecamp templates copy-status 5"}, - {name: "completed", body: completed, summary: "Template copy complete: Project kickoff (to-do list #10)", breadcrumb: "basecamp todolists show 10 --in 123"}, - {name: "failed", body: `{"id":5,"status":"failed","source_recording_id":3,"destination_parent_id":9,"url":"https://example.test/copies/5.json"}`, summary: "Template copy #5 failed", breadcrumb: "basecamp templates library"}, + {name: "pending", body: templateCopyPendingJSON, summary: "Template duplication #5 is pending", breadcrumb: "basecamp templates todolists duplication 5"}, + {name: "processing", body: `{"id":5,"status":"processing","source_recording_id":3,"destination_parent_id":9,"url":"https://example.test/copies/5.json"}`, summary: "Template duplication #5 is processing", breadcrumb: "basecamp templates todolists duplication 5"}, + {name: "completed", body: completed, summary: "Template duplication complete: Project kickoff (to-do list #10)", breadcrumb: "basecamp todolists show 10 --in 123"}, + {name: "failed", body: `{"id":5,"status":"failed","source_recording_id":3,"destination_parent_id":9,"url":"https://example.test/copies/5.json"}`, summary: "Template duplication #5 failed", breadcrumb: "basecamp templates todolists list"}, } for _, test := range tests { @@ -369,3 +367,126 @@ func TestTemplatesCopyStatusStates(t *testing.T) { }) } } + +func TestTemplatesCardTablesListUsesCardTablesEndpoint(t *testing.T) { + app, transport := setupRecordingTestApp(t, stubRoute{ + method: http.MethodGet, + path: "/99999/template_library/card_tables.json", + status: http.StatusOK, + body: `{"bucket":{"id":1,"name":"Templates","type":"TemplateLibrary"},"kanban_boardset":{"id":8,"title":"Boards","type":"Kanban::Boardset","url":"https://example.test/boardset.json","app_url":"https://example.test/boardset"},"card_tables":[{"id":12,"title":"Launch board","type":"Kanban::Board"}]}`, + }) + buf := captureTemplateOutput(app) + app.Config.ActiveProfile = "work profile" + app.Config.Sources = map[string]string{"account_id": string(config.SourceFlag)} + + err := executeRecordingCommand(NewTemplatesCmd(), app, "card-tables", "list") + require.NoError(t, err) + + assert.Equal(t, "/99999/template_library/card_tables.json", transport.last(t).Path) + envelope := decodeTemplateEnvelope(t, buf) + assert.Equal(t, "1 active card table templates", envelope.Summary) + require.Len(t, envelope.Breadcrumbs, 1) + assert.Equal(t, "basecamp templates card-tables duplicate <template-id> --in <project> --profile 'work profile' --account 99999", envelope.Breadcrumbs[0].Cmd) +} + +func TestTemplatesCardTablesDuplicateNamesTheProject(t *testing.T) { + app, transport := setupRecordingTestApp(t, + projectsRoute(), + stubRoute{ + method: http.MethodPost, + path: "/99999/template_library/copies.json", + status: http.StatusCreated, + body: templateCopyPendingJSON, + }, + ) + buf := captureTemplateOutput(app) + + err := executeRecordingCommand(NewTemplatesCmd(), app, "card-tables", "duplicate", "3", "--in", "Test Project") + require.NoError(t, err) + + requests := transport.recorded() + post := requests[len(requests)-1] + assert.Equal(t, "/99999/template_library/copies.json", post.Path) + assert.JSONEq(t, `{"template_recording_id":3,"destination_project_id":123}`, post.Body) + + envelope := decodeTemplateEnvelope(t, buf) + assert.Equal(t, "Started template duplication #5 (pending)", envelope.Summary) + require.Len(t, envelope.Breadcrumbs, 1) + assert.Equal(t, "basecamp templates card-tables duplication 5", envelope.Breadcrumbs[0].Cmd) +} + +func TestTemplatesCardTablesDuplicateHasNoTodosetFlag(t *testing.T) { + cmd, _, err := NewTemplatesCmd().Find([]string{"card-tables", "duplicate"}) + require.NoError(t, err) + assert.Nil(t, cmd.Flags().Lookup("todoset"), "a project has one dock, so there is nothing to pin") + assert.NotNil(t, cmd.Flags().Lookup("in")) +} + +func TestTemplatifySendsEmptyBodyWithoutFlags(t *testing.T) { + app, transport := setupRecordingTestApp(t, + projectsRoute(), + stubRoute{ + method: http.MethodPost, + path: "/99999/buckets/123/recordings/55/templatifications.json", + status: http.StatusCreated, + body: `{"id":9,"status":"pending","source_recording_id":55,"url":"https://example.test/t/9.json"}`, + }, + ) + buf := captureTemplateOutput(app) + + err := executeRecordingCommand(NewTodolistsCmd(), app, "templatify", "55", "--in", "123") + require.NoError(t, err) + + post := transport.recorded()[len(transport.recorded())-1] + assert.JSONEq(t, `{}`, post.Body, "every attribute defaults server-side, so no flags means no body") + + envelope := decodeTemplateEnvelope(t, buf) + assert.Equal(t, "Started saving to-do list #55 as a template (pending)", envelope.Summary) +} + +func TestTemplatifyOffersTriageOnlyForCardTables(t *testing.T) { + todolist, _, err := NewTodolistsCmd().Find([]string{"templatify"}) + require.NoError(t, err) + assert.Nil(t, todolist.Flags().Lookup("move-cards-to-triage"), "bc3 never reads it for a to-do list") + + cardTable, _, err := NewCardTablesCmd().Find([]string{"templatify"}) + require.NoError(t, err) + assert.NotNil(t, cardTable.Flags().Lookup("move-cards-to-triage")) +} + +func TestTemplatificationReportsTheTemplateItMade(t *testing.T) { + app, _ := setupRecordingTestApp(t, + projectsRoute(), + stubRoute{ + method: http.MethodGet, + path: "/99999/buckets/123/recordings/55/templatifications/9", + status: http.StatusOK, + body: `{"id":9,"status":"completed","source_recording_id":55,"url":"https://example.test/t/9.json","destination_todolist":{"id":70,"name":"Client onboarding","title":"Client onboarding","bucket":{"id":1,"name":"Templates","type":"TemplateLibrary"}}}`, + }, + ) + buf := captureTemplateOutput(app) + + err := executeRecordingCommand(NewTodolistsCmd(), app, "templatification", "55", "9", "--in", "123") + require.NoError(t, err) + + envelope := decodeTemplateEnvelope(t, buf) + assert.Equal(t, "Saved as template: Client onboarding (to-do list template #70)", envelope.Summary) + require.Len(t, envelope.Breadcrumbs, 1) + assert.Equal(t, "basecamp templates todolists list", envelope.Breadcrumbs[0].Cmd) +} + +func TestTemplatesTodolistsCreateSendsNameAndDescription(t *testing.T) { + app, transport := setupRecordingTestApp(t, stubRoute{ + method: http.MethodPost, + path: "/99999/template_library/todolists.json", + status: http.StatusCreated, + body: `{"id":71,"name":"New hire setup","title":"New hire setup"}`, + }) + buf := captureTemplateOutput(app) + + err := executeRecordingCommand(NewTemplatesCmd(), app, "todolists", "create", "New hire setup", "--description", "Day one") + require.NoError(t, err) + + assert.JSONEq(t, `{"name":"New hire setup","description":"Day one"}`, transport.last(t).Body) + assert.Equal(t, "Created to-do list template #71: New hire setup", decodeTemplateEnvelope(t, buf).Summary) +} diff --git a/internal/commands/todolists.go b/internal/commands/todolists.go index dbd20f63..edd2c790 100644 --- a/internal/commands/todolists.go +++ b/internal/commands/todolists.go @@ -40,6 +40,8 @@ to disambiguate when needed.`, newTodolistsCreateCmd(&project, &todosetID), newTodolistsUpdateCmd(&project), newTodolistsPositionCmd(&project), + newTemplatifyCmd(todolistTemplatificationKind(), &project), + newTemplatificationCmd(todolistTemplatificationKind(), &project), newRecordableTrashCmd("todolist"), newRecordableArchiveCmd("todolist"), newRecordableRestoreCmd("todolist"), diff --git a/skills/basecamp/SKILL.md b/skills/basecamp/SKILL.md index 9a004a31..54f4f3f5 100644 --- a/skills/basecamp/SKILL.md +++ b/skills/basecamp/SKILL.md @@ -969,30 +969,68 @@ basecamp recordings visibility <id> --hidden # Hide from clients ### Templates +Templates are grouped by the kind of template each verb operates on. + ```bash -basecamp templates list --json # List project templates -basecamp templates show <id> --json # Project template details -basecamp templates create "Template Name" # Create empty project template -basecamp templates update <id> --name "New Name" -basecamp templates delete <id> # Trash project template -basecamp templates construct <id> --name "New Project" # Create project (async) -basecamp templates construct <id> --name "New Project" --start-date 2026-09-01 # Anchor template dates to that week -basecamp templates construction <template_id> <construction_id> # Check project status +basecamp templates projects list --json # List project templates +basecamp templates projects show <id> --json # Project template details +basecamp templates projects create "Template Name" # Create empty project template +basecamp templates projects update <id> --name "New Name" +basecamp templates projects delete <id> # Trash project template +basecamp templates projects construct <id> --name "New Project" # Create project (async) +basecamp templates projects construct <id> --name "New Project" --start-date 2026-09-01 # Anchor template dates to that week +basecamp templates projects construction <template_id> <construction_id> # Check project status + +basecamp templates todolists list --json # List active to-do list templates +basecamp templates todolists create "List Name" # Create empty to-do list template +basecamp templates todolists duplicate <template_id> --in <project> # Duplicate into To-dos (async) +basecamp templates todolists duplication <id> # Check duplication status -basecamp templates library --json # List active to-do list templates -basecamp templates copy <template_id> --in <project> # Start copying into To-dos -basecamp templates copy-status <copy_id> # Check copy status +basecamp templates card-tables list --json # List active card table templates +basecamp templates card-tables create "Board Name" # Create empty card table template +basecamp templates card-tables duplicate <template_id> --in <project> # Duplicate into project (async) +basecamp templates card-tables duplication <id> # Check duplication status ``` +Save existing work into the library from the source group, mirroring the +product, where "Save as a template..." is an action on the item itself: + +```bash +basecamp todolists templatify <id> --in <project> # Save a list (async) +basecamp todolists templatification <id> <tid> --in <project> # Check save status +basecamp card-tables templatify <id> --in <project> --move-cards-to-triage +basecamp card-tables templatification <id> <tid> --in <project> +``` + +A templatification is the record of a save in progress, named to match +`construction` and `duplication`. Every attribute of a save defaults server-side, so `templatify <id>` with +no flags is a complete request and the template takes the source's title. +`--move-cards-to-triage` gathers the cards into Triage instead of leaving them +where they sit, and is card tables only. + +`duplicate` names the destination project and Basecamp picks the container from +the template's kind: a to-do list lands in the project's To-dos tool, a card +table on its dock. `--todoset` overrides that for a to-do list when a project +has more than one; `card-tables duplicate` has no container flag because a +project has exactly one dock. To-do list +templates land in the project's To-dos tool, which `--todoset` can pin. + +**Older spellings still work.** The flat commands that shipped before the grouping +(`templates list`, `templates show`, `templates create`, `templates update`, +`templates delete`, `templates construct`, `templates construction`, +`templates library`, `templates copy`, `templates copy-status`) remain as aliases. +`copy` and `copy-status` also work inside `templates todolists`. Prefer the grouped, +canonical spellings above when writing new commands. + **Asynchronous results:** `construct` returns a construction ID; poll `construction` until `status="completed"` to get the project. A template's dates are relative to its first week, and template weeks start on Sunday: `--start-date` (YYYY-MM-DD or natural, e.g. `next monday`) anchors them to the Sunday on or before that date; without it they -anchor to the week of construction. `copy` returns a copy ID; poll -`copy-status` through `pending` and `processing` until it is `completed` or `failed`. +anchor to the week of construction. `duplicate` returns a duplication ID; poll +`duplication` through `pending` and `processing` until it is `completed` or `failed`. -A copy can report the people who need access to the destination project. Show those -people to the user and rerun with `--confirm-adding-people` only after the user +A duplication can report the people who need access to the destination project. Show +those people to the user and rerun with `--confirm-adding-people` only after the user explicitly approves granting that access. Never add this flag automatically. ### Webhooks