diff --git a/.mise.toml b/.mise.toml index f1ac9ee05..92e2c37ac 100644 --- a/.mise.toml +++ b/.mise.toml @@ -4,6 +4,9 @@ go = "1.26" goreleaser = "2.15.4" # Keep in sync with .github/workflows/test.yml rhysd/actionlint version pin. actionlint = "1.7.12" +# Runs the e2e suite. Keep in sync with the bats-core tag installed in +# .github/workflows/test.yml and .github/workflows/release.yml. +bats = "1.11.0" # Not invoked directly; actionlint shells out to it to lint workflow `run:` # scripts (CI uses the runner's preinstalled shellcheck). shellcheck = "0.11.0" diff --git a/.surface b/.surface index d24af0f97..1f91205b2 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,15 +339,29 @@ 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 construct 00 <template_id> -ARG basecamp templates construction 00 <template_id> -ARG basecamp templates construction 01 <construction_id> -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 show 00 <id> -ARG basecamp templates update 00 <id> +ARG basecamp templates card-tables archive 00 <template_id> +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 card-tables restore 00 <template_id> +ARG basecamp templates card-tables trash 00 <template_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 todolists archive 00 <template_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 todolists restore 00 <template_id> +ARG basecamp templates todolists trash 00 <template_id> ARG basecamp timeline 00 [me] ARG basecamp timesheet item 00 <id> ARG basecamp timesheet recording 00 <id> @@ -371,6 +388,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 +416,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> @@ -562,6 +585,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 @@ -1003,16 +1029,34 @@ CMD basecamp subscriptions show CMD basecamp subscriptions subscribe CMD basecamp subscriptions unsubscribe CMD basecamp templates -CMD basecamp templates construct -CMD basecamp templates construction -CMD basecamp templates copy -CMD basecamp templates copy-status -CMD basecamp templates create -CMD basecamp templates delete -CMD basecamp templates library -CMD basecamp templates list -CMD basecamp templates show -CMD basecamp templates update +CMD basecamp templates card-tables +CMD basecamp templates card-tables archive +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 card-tables restore +CMD basecamp templates card-tables trash +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 todolists +CMD basecamp templates todolists archive +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 todolists restore +CMD basecamp templates todolists trash CMD basecamp timeline CMD basecamp timesheet CMD basecamp timesheet item @@ -1044,6 +1088,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 @@ -1071,6 +1117,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 @@ -2876,6 +2924,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 @@ -13467,229 +13582,615 @@ 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 construct --account type=string -FLAG basecamp templates construct --agent type=bool -FLAG basecamp templates construct --cache-dir type=string -FLAG basecamp templates construct --count type=bool -FLAG basecamp templates construct --desc type=string -FLAG basecamp templates construct --description type=string -FLAG basecamp templates construct --help type=bool -FLAG basecamp templates construct --hints type=bool -FLAG basecamp templates construct --ids-only type=bool -FLAG basecamp templates construct --in type=string -FLAG basecamp templates construct --jq type=string -FLAG basecamp templates construct --json type=bool -FLAG basecamp templates construct --markdown type=bool -FLAG basecamp templates construct --md type=bool -FLAG basecamp templates construct --name type=string -FLAG basecamp templates construct --no-hints type=bool -FLAG basecamp templates construct --no-stats type=bool -FLAG basecamp templates construct --profile type=string -FLAG basecamp templates construct --project type=string -FLAG basecamp templates construct --quiet type=bool -FLAG basecamp templates construct --start-date type=string -FLAG basecamp templates construct --stats type=bool -FLAG basecamp templates construct --styled type=bool -FLAG basecamp templates construct --todolist type=string -FLAG basecamp templates construct --verbose type=count -FLAG basecamp templates construction --account type=string -FLAG basecamp templates construction --agent type=bool -FLAG basecamp templates construction --cache-dir type=string -FLAG basecamp templates construction --count type=bool -FLAG basecamp templates construction --help type=bool -FLAG basecamp templates construction --hints type=bool -FLAG basecamp templates construction --ids-only type=bool -FLAG basecamp templates construction --in type=string -FLAG basecamp templates construction --jq type=string -FLAG basecamp templates construction --json type=bool -FLAG basecamp templates construction --markdown type=bool -FLAG basecamp templates construction --md type=bool -FLAG basecamp templates construction --no-hints type=bool -FLAG basecamp templates construction --no-stats type=bool -FLAG basecamp templates construction --profile type=string -FLAG basecamp templates construction --project type=string -FLAG basecamp templates construction --quiet type=bool -FLAG basecamp templates construction --stats type=bool -FLAG basecamp templates construction --styled type=bool -FLAG basecamp templates construction --todolist type=string -FLAG basecamp templates construction --verbose type=count -FLAG basecamp templates copy --account type=string -FLAG basecamp templates copy --agent type=bool -FLAG basecamp templates copy --cache-dir type=string -FLAG basecamp templates copy --confirm-adding-people type=bool -FLAG basecamp templates copy --count type=bool -FLAG basecamp templates copy --help type=bool -FLAG basecamp templates copy --hints type=bool -FLAG basecamp templates copy --ids-only type=bool -FLAG basecamp templates copy --in type=string -FLAG basecamp templates copy --jq type=string -FLAG basecamp templates copy --json type=bool -FLAG basecamp templates copy --markdown type=bool -FLAG basecamp templates copy --md type=bool -FLAG basecamp templates copy --no-hints type=bool -FLAG basecamp templates copy --no-stats type=bool -FLAG basecamp templates copy --profile type=string -FLAG basecamp templates copy --project type=string -FLAG basecamp templates copy --quiet type=bool -FLAG basecamp templates copy --stats type=bool -FLAG basecamp templates copy --styled type=bool -FLAG basecamp templates copy --todolist type=string -FLAG basecamp templates copy --todoset type=string -FLAG basecamp templates copy --verbose type=count -FLAG basecamp templates copy-status --account type=string -FLAG basecamp templates copy-status --agent type=bool -FLAG basecamp templates copy-status --cache-dir type=string -FLAG basecamp templates copy-status --count type=bool -FLAG basecamp templates copy-status --help type=bool -FLAG basecamp templates copy-status --hints type=bool -FLAG basecamp templates copy-status --ids-only type=bool -FLAG basecamp templates copy-status --in type=string -FLAG basecamp templates copy-status --jq type=string -FLAG basecamp templates copy-status --json type=bool -FLAG basecamp templates copy-status --markdown type=bool -FLAG basecamp templates copy-status --md type=bool -FLAG basecamp templates copy-status --no-hints type=bool -FLAG basecamp templates copy-status --no-stats type=bool -FLAG basecamp templates copy-status --profile type=string -FLAG basecamp templates copy-status --project type=string -FLAG basecamp templates copy-status --quiet type=bool -FLAG basecamp templates copy-status --stats type=bool -FLAG basecamp templates copy-status --styled type=bool -FLAG basecamp templates copy-status --todolist type=string -FLAG basecamp templates copy-status --verbose type=count -FLAG basecamp templates create --account type=string -FLAG basecamp templates create --agent type=bool -FLAG basecamp templates create --cache-dir type=string -FLAG basecamp templates create --count type=bool -FLAG basecamp templates create --desc type=string -FLAG basecamp templates create --description type=string -FLAG basecamp templates create --help type=bool -FLAG basecamp templates create --hints type=bool -FLAG basecamp templates create --ids-only type=bool -FLAG basecamp templates create --in type=string -FLAG basecamp templates create --jq type=string -FLAG basecamp templates create --json type=bool -FLAG basecamp templates create --markdown type=bool -FLAG basecamp templates create --md type=bool -FLAG basecamp templates create --name type=string -FLAG basecamp templates create --no-hints type=bool -FLAG basecamp templates create --no-stats type=bool -FLAG basecamp templates create --profile type=string -FLAG basecamp templates create --project type=string -FLAG basecamp templates create --quiet type=bool -FLAG basecamp templates create --stats type=bool -FLAG basecamp templates create --styled type=bool -FLAG basecamp templates create --todolist type=string -FLAG basecamp templates create --verbose type=count -FLAG basecamp templates delete --account type=string -FLAG basecamp templates delete --agent type=bool -FLAG basecamp templates delete --cache-dir type=string -FLAG basecamp templates delete --count type=bool -FLAG basecamp templates delete --help type=bool -FLAG basecamp templates delete --hints type=bool -FLAG basecamp templates delete --ids-only type=bool -FLAG basecamp templates delete --in type=string -FLAG basecamp templates delete --jq type=string -FLAG basecamp templates delete --json type=bool -FLAG basecamp templates delete --markdown type=bool -FLAG basecamp templates delete --md type=bool -FLAG basecamp templates delete --no-hints type=bool -FLAG basecamp templates delete --no-stats type=bool -FLAG basecamp templates delete --profile type=string -FLAG basecamp templates delete --project type=string -FLAG basecamp templates delete --quiet type=bool -FLAG basecamp templates delete --stats type=bool -FLAG basecamp templates delete --styled type=bool -FLAG basecamp templates delete --todolist type=string -FLAG basecamp templates delete --verbose type=count -FLAG basecamp templates library --account type=string -FLAG basecamp templates library --agent type=bool -FLAG basecamp templates library --cache-dir type=string -FLAG basecamp templates library --count type=bool -FLAG basecamp templates library --help type=bool -FLAG basecamp templates library --hints type=bool -FLAG basecamp templates library --ids-only type=bool -FLAG basecamp templates library --in type=string -FLAG basecamp templates library --jq type=string -FLAG basecamp templates library --json type=bool -FLAG basecamp templates library --markdown type=bool -FLAG basecamp templates library --md type=bool -FLAG basecamp templates library --no-hints type=bool -FLAG basecamp templates library --no-stats type=bool -FLAG basecamp templates library --profile type=string -FLAG basecamp templates library --project type=string -FLAG basecamp templates library --quiet type=bool -FLAG basecamp templates library --stats type=bool -FLAG basecamp templates library --styled type=bool -FLAG basecamp templates library --todolist type=string -FLAG basecamp templates library --verbose type=count -FLAG basecamp templates list --account type=string -FLAG basecamp templates list --agent type=bool -FLAG basecamp templates list --cache-dir type=string -FLAG basecamp templates list --count type=bool -FLAG basecamp templates list --help type=bool -FLAG basecamp templates list --hints type=bool -FLAG basecamp templates list --ids-only type=bool -FLAG basecamp templates list --in type=string -FLAG basecamp templates list --jq type=string -FLAG basecamp templates list --json type=bool -FLAG basecamp templates list --markdown type=bool -FLAG basecamp templates list --md type=bool -FLAG basecamp templates list --no-hints type=bool -FLAG basecamp templates list --no-stats type=bool -FLAG basecamp templates list --profile type=string -FLAG basecamp templates list --project type=string -FLAG basecamp templates list --quiet type=bool -FLAG basecamp templates list --stats type=bool -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 show --account type=string -FLAG basecamp templates show --agent type=bool -FLAG basecamp templates show --cache-dir type=string -FLAG basecamp templates show --count type=bool -FLAG basecamp templates show --help type=bool -FLAG basecamp templates show --hints type=bool -FLAG basecamp templates show --ids-only type=bool -FLAG basecamp templates show --in type=string -FLAG basecamp templates show --jq type=string -FLAG basecamp templates show --json type=bool -FLAG basecamp templates show --markdown type=bool -FLAG basecamp templates show --md type=bool -FLAG basecamp templates show --no-hints type=bool -FLAG basecamp templates show --no-stats type=bool -FLAG basecamp templates show --profile type=string -FLAG basecamp templates show --project type=string -FLAG basecamp templates show --quiet type=bool -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 update --account type=string -FLAG basecamp templates update --agent type=bool -FLAG basecamp templates update --cache-dir type=string -FLAG basecamp templates update --count type=bool -FLAG basecamp templates update --desc type=string -FLAG basecamp templates update --description type=string -FLAG basecamp templates update --help type=bool -FLAG basecamp templates update --hints type=bool -FLAG basecamp templates update --ids-only type=bool -FLAG basecamp templates update --in type=string -FLAG basecamp templates update --jq type=string -FLAG basecamp templates update --json type=bool -FLAG basecamp templates update --markdown type=bool -FLAG basecamp templates update --md type=bool -FLAG basecamp templates update --name type=string -FLAG basecamp templates update --no-hints type=bool -FLAG basecamp templates update --no-stats type=bool -FLAG basecamp templates update --profile type=string -FLAG basecamp templates update --project type=string -FLAG basecamp templates update --quiet type=bool -FLAG basecamp templates update --stats type=bool -FLAG basecamp templates update --styled type=bool -FLAG basecamp templates update --todolist type=string -FLAG basecamp templates update --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 archive --account type=string +FLAG basecamp templates card-tables archive --agent type=bool +FLAG basecamp templates card-tables archive --cache-dir type=string +FLAG basecamp templates card-tables archive --count type=bool +FLAG basecamp templates card-tables archive --help type=bool +FLAG basecamp templates card-tables archive --hints type=bool +FLAG basecamp templates card-tables archive --ids-only type=bool +FLAG basecamp templates card-tables archive --in type=string +FLAG basecamp templates card-tables archive --jq type=string +FLAG basecamp templates card-tables archive --json type=bool +FLAG basecamp templates card-tables archive --markdown type=bool +FLAG basecamp templates card-tables archive --md type=bool +FLAG basecamp templates card-tables archive --no-hints type=bool +FLAG basecamp templates card-tables archive --no-stats type=bool +FLAG basecamp templates card-tables archive --profile type=string +FLAG basecamp templates card-tables archive --project type=string +FLAG basecamp templates card-tables archive --quiet type=bool +FLAG basecamp templates card-tables archive --stats type=bool +FLAG basecamp templates card-tables archive --styled type=bool +FLAG basecamp templates card-tables archive --todolist type=string +FLAG basecamp templates card-tables archive --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 card-tables restore --account type=string +FLAG basecamp templates card-tables restore --agent type=bool +FLAG basecamp templates card-tables restore --cache-dir type=string +FLAG basecamp templates card-tables restore --count type=bool +FLAG basecamp templates card-tables restore --help type=bool +FLAG basecamp templates card-tables restore --hints type=bool +FLAG basecamp templates card-tables restore --ids-only type=bool +FLAG basecamp templates card-tables restore --in type=string +FLAG basecamp templates card-tables restore --jq type=string +FLAG basecamp templates card-tables restore --json type=bool +FLAG basecamp templates card-tables restore --markdown type=bool +FLAG basecamp templates card-tables restore --md type=bool +FLAG basecamp templates card-tables restore --no-hints type=bool +FLAG basecamp templates card-tables restore --no-stats type=bool +FLAG basecamp templates card-tables restore --profile type=string +FLAG basecamp templates card-tables restore --project type=string +FLAG basecamp templates card-tables restore --quiet type=bool +FLAG basecamp templates card-tables restore --stats type=bool +FLAG basecamp templates card-tables restore --styled type=bool +FLAG basecamp templates card-tables restore --todolist type=string +FLAG basecamp templates card-tables restore --verbose type=count +FLAG basecamp templates card-tables trash --account type=string +FLAG basecamp templates card-tables trash --agent type=bool +FLAG basecamp templates card-tables trash --cache-dir type=string +FLAG basecamp templates card-tables trash --count type=bool +FLAG basecamp templates card-tables trash --help type=bool +FLAG basecamp templates card-tables trash --hints type=bool +FLAG basecamp templates card-tables trash --ids-only type=bool +FLAG basecamp templates card-tables trash --in type=string +FLAG basecamp templates card-tables trash --jq type=string +FLAG basecamp templates card-tables trash --json type=bool +FLAG basecamp templates card-tables trash --markdown type=bool +FLAG basecamp templates card-tables trash --md type=bool +FLAG basecamp templates card-tables trash --no-hints type=bool +FLAG basecamp templates card-tables trash --no-stats type=bool +FLAG basecamp templates card-tables trash --profile type=string +FLAG basecamp templates card-tables trash --project type=string +FLAG basecamp templates card-tables trash --quiet type=bool +FLAG basecamp templates card-tables trash --stats type=bool +FLAG basecamp templates card-tables trash --styled type=bool +FLAG basecamp templates card-tables trash --todolist type=string +FLAG basecamp templates card-tables trash --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 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 archive --account type=string +FLAG basecamp templates todolists archive --agent type=bool +FLAG basecamp templates todolists archive --cache-dir type=string +FLAG basecamp templates todolists archive --count type=bool +FLAG basecamp templates todolists archive --help type=bool +FLAG basecamp templates todolists archive --hints type=bool +FLAG basecamp templates todolists archive --ids-only type=bool +FLAG basecamp templates todolists archive --in type=string +FLAG basecamp templates todolists archive --jq type=string +FLAG basecamp templates todolists archive --json type=bool +FLAG basecamp templates todolists archive --markdown type=bool +FLAG basecamp templates todolists archive --md type=bool +FLAG basecamp templates todolists archive --no-hints type=bool +FLAG basecamp templates todolists archive --no-stats type=bool +FLAG basecamp templates todolists archive --profile type=string +FLAG basecamp templates todolists archive --project type=string +FLAG basecamp templates todolists archive --quiet type=bool +FLAG basecamp templates todolists archive --stats type=bool +FLAG basecamp templates todolists archive --styled type=bool +FLAG basecamp templates todolists archive --todolist type=string +FLAG basecamp templates todolists archive --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 todolists restore --account type=string +FLAG basecamp templates todolists restore --agent type=bool +FLAG basecamp templates todolists restore --cache-dir type=string +FLAG basecamp templates todolists restore --count type=bool +FLAG basecamp templates todolists restore --help type=bool +FLAG basecamp templates todolists restore --hints type=bool +FLAG basecamp templates todolists restore --ids-only type=bool +FLAG basecamp templates todolists restore --in type=string +FLAG basecamp templates todolists restore --jq type=string +FLAG basecamp templates todolists restore --json type=bool +FLAG basecamp templates todolists restore --markdown type=bool +FLAG basecamp templates todolists restore --md type=bool +FLAG basecamp templates todolists restore --no-hints type=bool +FLAG basecamp templates todolists restore --no-stats type=bool +FLAG basecamp templates todolists restore --profile type=string +FLAG basecamp templates todolists restore --project type=string +FLAG basecamp templates todolists restore --quiet type=bool +FLAG basecamp templates todolists restore --stats type=bool +FLAG basecamp templates todolists restore --styled type=bool +FLAG basecamp templates todolists restore --todolist type=string +FLAG basecamp templates todolists restore --verbose type=count +FLAG basecamp templates todolists trash --account type=string +FLAG basecamp templates todolists trash --agent type=bool +FLAG basecamp templates todolists trash --cache-dir type=string +FLAG basecamp templates todolists trash --count type=bool +FLAG basecamp templates todolists trash --help type=bool +FLAG basecamp templates todolists trash --hints type=bool +FLAG basecamp templates todolists trash --ids-only type=bool +FLAG basecamp templates todolists trash --in type=string +FLAG basecamp templates todolists trash --jq type=string +FLAG basecamp templates todolists trash --json type=bool +FLAG basecamp templates todolists trash --markdown type=bool +FLAG basecamp templates todolists trash --md type=bool +FLAG basecamp templates todolists trash --no-hints type=bool +FLAG basecamp templates todolists trash --no-stats type=bool +FLAG basecamp templates todolists trash --profile type=string +FLAG basecamp templates todolists trash --project type=string +FLAG basecamp templates todolists trash --quiet type=bool +FLAG basecamp templates todolists trash --stats type=bool +FLAG basecamp templates todolists trash --styled type=bool +FLAG basecamp templates todolists trash --todolist type=string +FLAG basecamp templates todolists trash --verbose type=count FLAG basecamp timeline --account type=string FLAG basecamp timeline --agent type=bool FLAG basecamp timeline --all type=bool @@ -14413,6 +14914,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 @@ -15025,6 +15571,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 @@ -18274,6 +18865,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 @@ -18715,16 +19309,34 @@ SUB basecamp subscriptions show SUB basecamp subscriptions subscribe SUB basecamp subscriptions unsubscribe SUB basecamp templates -SUB basecamp templates construct -SUB basecamp templates construction -SUB basecamp templates copy -SUB basecamp templates copy-status -SUB basecamp templates create -SUB basecamp templates delete -SUB basecamp templates library -SUB basecamp templates list -SUB basecamp templates show -SUB basecamp templates update +SUB basecamp templates card-tables +SUB basecamp templates card-tables archive +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 card-tables restore +SUB basecamp templates card-tables trash +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 todolists +SUB basecamp templates todolists archive +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 todolists restore +SUB basecamp templates todolists trash SUB basecamp timeline SUB basecamp timesheet SUB basecamp timesheet item @@ -18756,6 +19368,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 @@ -18783,6 +19397,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/.surface-breaking b/.surface-breaking index 840004f60..4fe732e4a 100644 --- a/.surface-breaking +++ b/.surface-breaking @@ -17,6 +17,15 @@ ARG basecamp message 00 <title> ARG basecamp message 01 [body] ARG basecamp react 00 <content> ARG basecamp reopen 00 <id|url>... +ARG basecamp templates construct 00 <template_id> +ARG basecamp templates construction 00 <template_id> +ARG basecamp templates construction 01 <construction_id> +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 show 00 <id> +ARG basecamp templates update 00 <id> ARG basecamp todo 00 <content> ARG basecamp unassign 00 <id> ARG basecamp unassign 00 <todo_id> @@ -70,6 +79,16 @@ CMD basecamp mcp CMD basecamp message CMD basecamp react CMD basecamp reopen +CMD basecamp templates construct +CMD basecamp templates construction +CMD basecamp templates copy +CMD basecamp templates copy-status +CMD basecamp templates create +CMD basecamp templates delete +CMD basecamp templates library +CMD basecamp templates list +CMD basecamp templates show +CMD basecamp templates update CMD basecamp todo CMD basecamp upload archive CMD basecamp upload doc @@ -676,12 +695,235 @@ FLAG basecamp reopen --todolist type=string FLAG basecamp reopen --verbose type=count FLAG basecamp reopen --version type=bool FLAG basecamp templates --status type=string +FLAG basecamp templates construct --account type=string +FLAG basecamp templates construct --agent type=bool +FLAG basecamp templates construct --cache-dir type=string +FLAG basecamp templates construct --count type=bool +FLAG basecamp templates construct --desc type=string +FLAG basecamp templates construct --description type=string +FLAG basecamp templates construct --help type=bool +FLAG basecamp templates construct --hints type=bool +FLAG basecamp templates construct --ids-only type=bool +FLAG basecamp templates construct --in type=string +FLAG basecamp templates construct --jq type=string +FLAG basecamp templates construct --json type=bool +FLAG basecamp templates construct --markdown type=bool +FLAG basecamp templates construct --md type=bool +FLAG basecamp templates construct --name type=string +FLAG basecamp templates construct --no-hints type=bool +FLAG basecamp templates construct --no-stats type=bool +FLAG basecamp templates construct --profile type=string +FLAG basecamp templates construct --project type=string +FLAG basecamp templates construct --quiet type=bool +FLAG basecamp templates construct --start-date type=string +FLAG basecamp templates construct --stats type=bool FLAG basecamp templates construct --status type=string +FLAG basecamp templates construct --styled type=bool +FLAG basecamp templates construct --todolist type=string +FLAG basecamp templates construct --verbose type=count +FLAG basecamp templates construction --account type=string +FLAG basecamp templates construction --agent type=bool +FLAG basecamp templates construction --cache-dir type=string +FLAG basecamp templates construction --count type=bool +FLAG basecamp templates construction --help type=bool +FLAG basecamp templates construction --hints type=bool +FLAG basecamp templates construction --ids-only type=bool +FLAG basecamp templates construction --in type=string +FLAG basecamp templates construction --jq type=string +FLAG basecamp templates construction --json type=bool +FLAG basecamp templates construction --markdown type=bool +FLAG basecamp templates construction --md type=bool +FLAG basecamp templates construction --no-hints type=bool +FLAG basecamp templates construction --no-stats type=bool +FLAG basecamp templates construction --profile type=string +FLAG basecamp templates construction --project type=string +FLAG basecamp templates construction --quiet type=bool +FLAG basecamp templates construction --stats type=bool FLAG basecamp templates construction --status type=string +FLAG basecamp templates construction --styled type=bool +FLAG basecamp templates construction --todolist type=string +FLAG basecamp templates construction --verbose type=count +FLAG basecamp templates copy --account type=string +FLAG basecamp templates copy --agent type=bool +FLAG basecamp templates copy --cache-dir type=string +FLAG basecamp templates copy --confirm-adding-people type=bool +FLAG basecamp templates copy --count type=bool +FLAG basecamp templates copy --help type=bool +FLAG basecamp templates copy --hints type=bool +FLAG basecamp templates copy --ids-only type=bool +FLAG basecamp templates copy --in type=string +FLAG basecamp templates copy --jq type=string +FLAG basecamp templates copy --json type=bool +FLAG basecamp templates copy --markdown type=bool +FLAG basecamp templates copy --md type=bool +FLAG basecamp templates copy --no-hints type=bool +FLAG basecamp templates copy --no-stats type=bool +FLAG basecamp templates copy --profile type=string +FLAG basecamp templates copy --project type=string +FLAG basecamp templates copy --quiet type=bool +FLAG basecamp templates copy --stats type=bool +FLAG basecamp templates copy --styled type=bool +FLAG basecamp templates copy --todolist type=string +FLAG basecamp templates copy --todoset type=string +FLAG basecamp templates copy --verbose type=count +FLAG basecamp templates copy-status --account type=string +FLAG basecamp templates copy-status --agent type=bool +FLAG basecamp templates copy-status --cache-dir type=string +FLAG basecamp templates copy-status --count type=bool +FLAG basecamp templates copy-status --help type=bool +FLAG basecamp templates copy-status --hints type=bool +FLAG basecamp templates copy-status --ids-only type=bool +FLAG basecamp templates copy-status --in type=string +FLAG basecamp templates copy-status --jq type=string +FLAG basecamp templates copy-status --json type=bool +FLAG basecamp templates copy-status --markdown type=bool +FLAG basecamp templates copy-status --md type=bool +FLAG basecamp templates copy-status --no-hints type=bool +FLAG basecamp templates copy-status --no-stats type=bool +FLAG basecamp templates copy-status --profile type=string +FLAG basecamp templates copy-status --project type=string +FLAG basecamp templates copy-status --quiet type=bool +FLAG basecamp templates copy-status --stats type=bool +FLAG basecamp templates copy-status --styled type=bool +FLAG basecamp templates copy-status --todolist type=string +FLAG basecamp templates copy-status --verbose type=count +FLAG basecamp templates create --account type=string +FLAG basecamp templates create --agent type=bool +FLAG basecamp templates create --cache-dir type=string +FLAG basecamp templates create --count type=bool +FLAG basecamp templates create --desc type=string +FLAG basecamp templates create --description type=string +FLAG basecamp templates create --help type=bool +FLAG basecamp templates create --hints type=bool +FLAG basecamp templates create --ids-only type=bool +FLAG basecamp templates create --in type=string +FLAG basecamp templates create --jq type=string +FLAG basecamp templates create --json type=bool +FLAG basecamp templates create --markdown type=bool +FLAG basecamp templates create --md type=bool +FLAG basecamp templates create --name type=string +FLAG basecamp templates create --no-hints type=bool +FLAG basecamp templates create --no-stats type=bool +FLAG basecamp templates create --profile type=string +FLAG basecamp templates create --project type=string +FLAG basecamp templates create --quiet type=bool +FLAG basecamp templates create --stats type=bool FLAG basecamp templates create --status type=string +FLAG basecamp templates create --styled type=bool +FLAG basecamp templates create --todolist type=string +FLAG basecamp templates create --verbose type=count +FLAG basecamp templates delete --account type=string +FLAG basecamp templates delete --agent type=bool +FLAG basecamp templates delete --cache-dir type=string +FLAG basecamp templates delete --count type=bool +FLAG basecamp templates delete --help type=bool +FLAG basecamp templates delete --hints type=bool +FLAG basecamp templates delete --ids-only type=bool +FLAG basecamp templates delete --in type=string +FLAG basecamp templates delete --jq type=string +FLAG basecamp templates delete --json type=bool +FLAG basecamp templates delete --markdown type=bool +FLAG basecamp templates delete --md type=bool +FLAG basecamp templates delete --no-hints type=bool +FLAG basecamp templates delete --no-stats type=bool +FLAG basecamp templates delete --profile type=string +FLAG basecamp templates delete --project type=string +FLAG basecamp templates delete --quiet type=bool +FLAG basecamp templates delete --stats type=bool FLAG basecamp templates delete --status type=string +FLAG basecamp templates delete --styled type=bool +FLAG basecamp templates delete --todolist type=string +FLAG basecamp templates delete --verbose type=count +FLAG basecamp templates library --account type=string +FLAG basecamp templates library --agent type=bool +FLAG basecamp templates library --cache-dir type=string +FLAG basecamp templates library --count type=bool +FLAG basecamp templates library --help type=bool +FLAG basecamp templates library --hints type=bool +FLAG basecamp templates library --ids-only type=bool +FLAG basecamp templates library --in type=string +FLAG basecamp templates library --jq type=string +FLAG basecamp templates library --json type=bool +FLAG basecamp templates library --markdown type=bool +FLAG basecamp templates library --md type=bool +FLAG basecamp templates library --no-hints type=bool +FLAG basecamp templates library --no-stats type=bool +FLAG basecamp templates library --profile type=string +FLAG basecamp templates library --project type=string +FLAG basecamp templates library --quiet type=bool +FLAG basecamp templates library --stats type=bool +FLAG basecamp templates library --styled type=bool +FLAG basecamp templates library --todolist type=string +FLAG basecamp templates library --verbose type=count +FLAG basecamp templates list --account type=string +FLAG basecamp templates list --agent type=bool +FLAG basecamp templates list --cache-dir type=string +FLAG basecamp templates list --count type=bool +FLAG basecamp templates list --help type=bool +FLAG basecamp templates list --hints type=bool +FLAG basecamp templates list --ids-only type=bool +FLAG basecamp templates list --in type=string +FLAG basecamp templates list --jq type=string +FLAG basecamp templates list --json type=bool +FLAG basecamp templates list --markdown type=bool +FLAG basecamp templates list --md type=bool +FLAG basecamp templates list --no-hints type=bool +FLAG basecamp templates list --no-stats type=bool +FLAG basecamp templates list --profile type=string +FLAG basecamp templates list --project type=string +FLAG basecamp templates list --quiet type=bool +FLAG basecamp templates list --stats type=bool +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 show --account type=string +FLAG basecamp templates show --agent type=bool +FLAG basecamp templates show --cache-dir type=string +FLAG basecamp templates show --count type=bool +FLAG basecamp templates show --help type=bool +FLAG basecamp templates show --hints type=bool +FLAG basecamp templates show --ids-only type=bool +FLAG basecamp templates show --in type=string +FLAG basecamp templates show --jq type=string +FLAG basecamp templates show --json type=bool +FLAG basecamp templates show --markdown type=bool +FLAG basecamp templates show --md type=bool +FLAG basecamp templates show --no-hints type=bool +FLAG basecamp templates show --no-stats type=bool +FLAG basecamp templates show --profile type=string +FLAG basecamp templates show --project type=string +FLAG basecamp templates show --quiet type=bool +FLAG basecamp templates show --stats type=bool FLAG basecamp templates show --status type=string +FLAG basecamp templates show --styled type=bool +FLAG basecamp templates show --todolist type=string +FLAG basecamp templates show --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 +FLAG basecamp templates update --count type=bool +FLAG basecamp templates update --desc type=string +FLAG basecamp templates update --description type=string +FLAG basecamp templates update --help type=bool +FLAG basecamp templates update --hints type=bool +FLAG basecamp templates update --ids-only type=bool +FLAG basecamp templates update --in type=string +FLAG basecamp templates update --jq type=string +FLAG basecamp templates update --json type=bool +FLAG basecamp templates update --markdown type=bool +FLAG basecamp templates update --md type=bool +FLAG basecamp templates update --name type=string +FLAG basecamp templates update --no-hints type=bool +FLAG basecamp templates update --no-stats type=bool +FLAG basecamp templates update --profile type=string +FLAG basecamp templates update --project type=string +FLAG basecamp templates update --quiet type=bool +FLAG basecamp templates update --stats type=bool FLAG basecamp templates update --status type=string +FLAG basecamp templates update --styled type=bool +FLAG basecamp templates update --todolist type=string +FLAG basecamp templates update --verbose type=count FLAG basecamp timeline --interval type=int FLAG basecamp timeline --watch type=bool FLAG basecamp tlgroup create --name type=string @@ -1009,6 +1251,16 @@ SUB basecamp mcp SUB basecamp message SUB basecamp react SUB basecamp reopen +SUB basecamp templates construct +SUB basecamp templates construction +SUB basecamp templates copy +SUB basecamp templates copy-status +SUB basecamp templates create +SUB basecamp templates delete +SUB basecamp templates library +SUB basecamp templates list +SUB basecamp templates show +SUB basecamp templates update SUB basecamp todo SUB basecamp upload archive SUB basecamp upload doc diff --git a/STYLE.md b/STYLE.md index 57933f538..f09166f37 100644 --- a/STYLE.md +++ b/STYLE.md @@ -40,6 +40,15 @@ the canonical `<group> <action>` form (`cards create`, `todos create`, `todos complete`, etc.). Introducing a new top-level verb that shadows an existing group noun is not allowed. +Where a group is split by the kind of thing it holds, every verb lives under the +kind it acts on. `templates` has `projects`, `todolists`, and `card-tables`; the +flat pre-grouping spellings (`templates list`, `templates show`, `templates +create`, `templates update`, `templates delete`, `templates construct`, +`templates construction`, `templates library`, `templates copy`, +`templates copy-status`) have been removed. Use `templates projects delete`, +never `templates delete` — a flat verb cannot say which kind it acts on, and the +three kinds hit different endpoints. + Shortcut commands without a sibling plural group — `search`, `url`, `recordings`, `timesheet`, `assignments`, `notifications`, `setup`, `completion` — may have both `RunE` and subcommands. `scripts/check-bare-groups.sh` enforces diff --git a/e2e/smoke/smoke_account.bats b/e2e/smoke/smoke_account.bats index f5168b405..44abd54cf 100644 --- a/e2e/smoke/smoke_account.bats +++ b/e2e/smoke/smoke_account.bats @@ -15,14 +15,27 @@ setup_file() { assert_json_not_null '.data[0].id' } -@test "templates list returns templates" { - run_smoke basecamp templates list --json +@test "templates projects list returns templates" { + run_smoke basecamp templates projects list --json assert_success assert_json_value '.ok' 'true' } -@test "templates library returns to-do list templates" { - run_smoke basecamp templates library --json +@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 "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' @@ -35,14 +48,14 @@ setup_file() { assert_json_not_null '.data.id' } -@test "templates show returns template detail" { +@test "templates projects show returns template detail" { local out - out=$(basecamp templates list --json 2>/dev/null) || mark_unverifiable "Cannot list templates" + 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 show "$tmpl_id" --json + run_smoke basecamp templates projects show "$tmpl_id" --json assert_success assert_json_value '.ok' 'true' assert_json_not_null '.data.id' @@ -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 238e6de08..761439019 100644 --- a/e2e/smoke/smoke_lifecycle.bats +++ b/e2e/smoke/smoke_lifecycle.bats @@ -93,34 +93,106 @@ load smoke_helper # --- Account-wide / dangerous mutations --- -@test "templates construct is out of scope" { +@test "templates projects construct is out of scope" { mark_out_of_scope "Creates project from template — account-wide mutation" } -@test "templates construction is out of scope" { - mark_out_of_scope "Depends on templates construct (OOS)" +@test "templates projects construction is out of scope" { + mark_out_of_scope "Depends on templates projects construct (OOS)" } -@test "templates copy is out of scope" { - mark_out_of_scope "Copies a to-do list into a project and may grant project access" +@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 copy-status is out of scope" { - mark_out_of_scope "Depends on templates copy (OOS)" +@test "templates todolists archive is out of scope" { + mark_out_of_scope "Takes a template out of the account library" +} + +@test "templates todolists trash is out of scope" { + mark_out_of_scope "Takes a template out of the account library" +} + +@test "templates todolists restore is out of scope" { + mark_out_of_scope "Account-wide template mutation" } -@test "templates create is out of scope" { +@test "templates card-tables create is out of scope" { mark_out_of_scope "Account-wide template mutation" } -@test "templates update is out of scope" { +@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 card-tables archive is out of scope" { + mark_out_of_scope "Takes a template out of the account library" +} + +@test "templates card-tables trash is out of scope" { + mark_out_of_scope "Takes a template out of the account library" +} + +@test "templates card-tables restore is out of scope" { mark_out_of_scope "Account-wide template mutation" } -@test "templates delete is out of scope" { +@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 435618c67..9ccb9bf8d 100644 --- a/e2e/templates.bats +++ b/e2e/templates.bats @@ -15,11 +15,11 @@ load test_helper # Show errors -@test "templates show without id shows error" { +@test "templates projects show without id shows error" { create_credentials create_global_config '{"account_id": 99999}' - run basecamp templates show + run basecamp templates projects show assert_failure assert_output_contains "ID required" } @@ -27,30 +27,30 @@ load test_helper # Create errors -@test "templates create without name shows error" { +@test "templates projects create without name shows error" { create_credentials create_global_config '{"account_id": 99999}' - run basecamp templates create + run basecamp templates projects create assert_failure assert_json_value '.error' '<name> required' assert_json_value '.code' 'usage' } -@test "templates create --name without value shows error" { +@test "templates projects create --name without value shows error" { create_credentials create_global_config '{"account_id": 99999}' - run basecamp templates create --name + run basecamp templates projects create --name assert_failure assert_output_contains "--name requires a value" } -@test "templates create --description without value shows error" { +@test "templates projects create --description without value shows error" { create_credentials create_global_config '{"account_id": 99999}' - run basecamp templates create "Test" --description + run basecamp templates projects create "Test" --description assert_failure assert_output_contains "--description requires a value" } @@ -58,20 +58,20 @@ load test_helper # Update errors -@test "templates update without id shows error" { +@test "templates projects update without id shows error" { create_credentials create_global_config '{"account_id": 99999}' - run basecamp templates update + run basecamp templates projects update assert_failure assert_output_contains "ID required" } -@test "templates update without fields shows error" { +@test "templates projects update without fields shows error" { create_credentials create_global_config '{"account_id": 99999}' - run basecamp templates update 123 + run basecamp templates projects update 123 assert_failure assert_output_contains "No update fields specified" } @@ -79,11 +79,11 @@ load test_helper # Delete errors -@test "templates delete without id shows error" { +@test "templates projects delete without id shows error" { create_credentials create_global_config '{"account_id": 99999}' - run basecamp templates delete + run basecamp templates projects delete assert_failure assert_output_contains "ID required" } @@ -91,125 +91,269 @@ load test_helper # Construct errors -@test "templates construct without template id shows error" { +@test "templates projects construct without template id shows error" { create_credentials create_global_config '{"account_id": 99999}' - run basecamp templates construct + run basecamp templates projects construct assert_failure assert_output_contains "ID required" } -@test "templates construct without project name shows error" { +@test "templates projects construct with malformed start date shows error" { create_credentials create_global_config '{"account_id": 99999}' - run basecamp templates construct 123 + run basecamp templates projects construct 123 --name "Project" --start-date someday assert_failure - assert_output_contains "name required" + assert_output_contains "Invalid start date" } -@test "templates construct with malformed start date shows error" { + +# Construction status errors + +@test "templates projects construction without template id shows error" { create_credentials create_global_config '{"account_id": 99999}' - run basecamp templates construct 123 --name "Project" --start-date someday + run basecamp templates projects construction assert_failure - assert_output_contains "Invalid start date" + assert_output_contains "ID required" } +@test "templates projects construction without construction id shows error" { + create_credentials + create_global_config '{"account_id": 99999}' -# Construction status errors + run basecamp templates projects construction 123 + # Cobra returns "accepts 2 arg(s)" error + assert_failure +} -@test "templates construction without template id shows error" { + +# Template library copy errors + +@test "templates todolists duplication without copy id shows error" { create_credentials create_global_config '{"account_id": 99999}' - run basecamp templates construction + run basecamp templates todolists duplication assert_failure assert_output_contains "ID required" } -@test "templates construction without construction id shows error" { +@test "templates todolists duplicate --confirm-adding-people is accepted" { + run basecamp templates todolists duplicate --help + assert_success + assert_output_contains "--confirm-adding-people" +} + + +# Flag parsing + +@test "templates projects list --status without value shows error" { create_credentials create_global_config '{"account_id": 99999}' - run basecamp templates construction 123 - # Cobra returns "accepts 2 arg(s)" error + run basecamp templates projects list --status assert_failure + assert_output_contains "--status requires a value" } -# Template library copy errors +# Help + +@test "templates --help shows help" { + create_credentials + create_global_config '{"account_id": 99999}' + + run basecamp templates --help + assert_success + assert_output_contains "basecamp templates" + assert_output_contains "projects" + assert_output_contains "todolists" + assert_output_contains "card-tables" +} -@test "templates copy without template id shows error" { + +# Unknown action + +@test "templates unknown action shows error" { create_credentials create_global_config '{"account_id": 99999}' - run basecamp templates copy + 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 "ID required" + assert_output_contains "name required" } -@test "templates copy-status without copy id shows error" { +@test "templates projects list --status with invalid value shows error" { create_credentials create_global_config '{"account_id": 99999}' - run basecamp templates copy-status + 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 copy --confirm-adding-people is accepted" { - run basecamp templates copy --help +@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}' -# Flag parsing + run basecamp templates todolists duplication + assert_failure + assert_output_contains "ID required" +} -@test "templates list --status without value shows error" { +@test "templates todolists copy is an alias for duplicate" { create_credentials create_global_config '{"account_id": 99999}' - run basecamp templates list --status + run basecamp templates todolists copy assert_failure - assert_output_contains "--status requires a value" + assert_output_contains "ID required" } -@test "templates list --status with invalid value shows error" { +@test "templates todolists copy-status is an alias for duplication" { create_credentials create_global_config '{"account_id": 99999}' - run basecamp templates list --status bogus + run basecamp templates todolists copy-status assert_failure - assert_output_contains "unknown --status value" + 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" +} -# Help +@test "templates card-tables create without name shows error" { + create_credentials + create_global_config '{"account_id": 99999}' -@test "templates --help shows help" { + 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 --help + 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 "basecamp templates" - assert_output_contains "construct" - assert_output_contains "construction" - assert_output_contains "library" - assert_output_contains "copy-status" + 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}' -# Unknown action + run basecamp templates card-tables duplication + assert_failure + assert_output_contains "ID required" +} -@test "templates unknown action shows error" { +@test "todolists templatify without id shows error" { create_credentials create_global_config '{"account_id": 99999}' - run basecamp templates foobar - # Command may show help or require project - just verify it runs + 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 d1b03d187..aeb73180b 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 000000000..c60ee441c --- /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 904216535..41be3b8e0 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 d295c3353..1f4feec22 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/stdin_integration_test.go b/internal/commands/stdin_integration_test.go index 3477cf881..5599a610c 100644 --- a/internal/commands/stdin_integration_test.go +++ b/internal/commands/stdin_integration_test.go @@ -278,8 +278,8 @@ func TestStdinResolvesBeforeAccountAndProject(t *testing.T) { {"schedule create --description", NewScheduleCmd, []string{ "create", "Title", "--starts-at", "2026-01-01T10:00:00Z", "--ends-at", "2026-01-01T11:00:00Z", "--description", "-", }}, - {"templates update --description", NewTemplatesCmd, []string{"update", "1", "--description", "-"}}, - {"templates construct --description", NewTemplatesCmd, []string{"construct", "1", "--name", "P", "--description", "-"}}, + {"templates projects update --description", NewTemplatesCmd, []string{"projects", "update", "1", "--description", "-"}}, + {"templates projects construct --description", NewTemplatesCmd, []string{"projects", "construct", "1", "--name", "P", "--description", "-"}}, } { t.Run(tc.name, func(t *testing.T) { transport := &countingTransport{} @@ -340,8 +340,8 @@ func TestMalformedIDRejectedBeforeReadingStdin(t *testing.T) { {"cards update", NewCardsCmd, []string{"update", "nope", "--body", "-"}}, {"cards column update", NewCardsCmd, []string{"column", "update", "nope", "--description", "-"}}, {"gauges update", NewGaugesCmd, []string{"update", "nope", "--description", "-"}}, - {"templates update", NewTemplatesCmd, []string{"update", "nope", "--description", "-"}}, - {"templates construct", NewTemplatesCmd, []string{"construct", "nope", "--name", "P", "--description", "-"}}, + {"templates projects update", NewTemplatesCmd, []string{"projects", "update", "nope", "--description", "-"}}, + {"templates projects construct", NewTemplatesCmd, []string{"projects", "construct", "nope", "--name", "P", "--description", "-"}}, {"messages update", NewMessagesCmd, []string{"update", "nope", "--body", "-"}}, {"todos update", NewTodosCmd, []string{"update", "nope", "--description", "-"}}, {"todolists update", NewTodolistsCmd, []string{"update", "nope", "--description", "-"}}, diff --git a/internal/commands/templates.go b/internal/commands/templates.go index a8482b3dc..9a2233d4c 100644 --- a/internal/commands/templates.go +++ b/internal/commands/templates.go @@ -1,6 +1,7 @@ package commands import ( + "context" "errors" "fmt" "strconv" @@ -20,12 +21,35 @@ 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. + +Every verb lives under the kind it acts on: templates projects delete, not +templates delete.`, + 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(), + ) + + 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,294 @@ 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(), + newTemplatesTodolistsCreateCmd(), + newTemplatesDuplicateCmd(kind, "duplicate <template_id>", "copy"), + newTemplatesDuplicationCmd(kind, "duplication <duplication_id>", "copy-status"), + ) + cmd.AddCommand(newTemplatesLibraryStatusCmds("todolists", "to-do list")...) + + return cmd +} + +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"), + ) + cmd.AddCommand(newTemplatesLibraryStatusCmds("card-tables", "card table")...) + + 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 +425,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() *cobra.Command { return &cobra.Command{ - Use: "library", + Use: "list", 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 +459,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 +478,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 +488,131 @@ func newTemplatesLibraryCmd() *cobra.Command { } } -func newTemplatesCopyCmd() *cobra.Command { +// templateStatusChange is one of the three status changes a library template +// can undergo, bundled so the run helper needs no dispatch of its own. +type templateStatusChange struct { + state string // status the template ends up in + past string // summary verb + apply func(*basecamp.RecordingsService, context.Context, int64) error +} + +func newTemplatesLibraryStatusCmds(group, noun string) []*cobra.Command { + archive := templateStatusChange{state: "archived", past: "Archived", apply: (*basecamp.RecordingsService).Archive} + trash := templateStatusChange{state: "trashed", past: "Trashed", apply: (*basecamp.RecordingsService).Trash} + restore := templateStatusChange{state: "active", past: "Restored", apply: (*basecamp.RecordingsService).Unarchive} + + return []*cobra.Command{ + { + Use: "archive <template_id>", + Short: fmt.Sprintf("Archive a %s template", noun), + Long: fmt.Sprintf(`Archive a %s template, taking it out of the library. + +Archiving keeps the template around: restore it later with +basecamp templates %s restore <template_id>.`, noun, group), + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return runTemplatesLibraryStatus(cmd, group, noun, args[0], archive) + }, + }, + { + Use: "trash <template_id>", + Short: fmt.Sprintf("Move a %s template to the trash", noun), + Long: fmt.Sprintf(`Move a %s template to the trash, taking it out of the library. + +Restore it with basecamp templates %s restore <template_id> while it is +still in the trash.`, noun, group), + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return runTemplatesLibraryStatus(cmd, group, noun, args[0], trash) + }, + }, + { + Use: "restore <template_id>", + Short: fmt.Sprintf("Restore a %s template", noun), + Long: fmt.Sprintf("Return an archived or trashed %s template to the library.", noun), + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return runTemplatesLibraryStatus(cmd, group, noun, args[0], restore) + }, + }, + } +} + +func runTemplatesLibraryStatus(cmd *cobra.Command, group, noun, templateIDArg string, change templateStatusChange) 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, + ) + + templateID, err := strconv.ParseInt(templateIDArg, 10, 64) + if err != nil { + return output.ErrUsage("Invalid template ID") + } + + // A library template is a recording, so its status changes go through the + // recordings endpoint; the project-template routes 404 for one. + if err := change.apply(app.Account().Recordings(), cmd.Context(), templateID); err != nil { + return convertSDKError(err) + } + + return app.OK(map[string]any{"id": templateID, "status": change.state}, + output.WithSummary(fmt.Sprintf("%s %s template #%d", change.past, noun, templateID)), + output.WithBreadcrumbs( + output.Breadcrumb{ + Action: "list", + Cmd: fmt.Sprintf("basecamp templates %s list%s", group, contextArgs), + Description: fmt.Sprintf("List %s templates", noun), + }, + templateStatusFollowUp(group, templateID, contextArgs, change), + ), + ) +} + +func templateStatusFollowUp(group string, templateID int64, contextArgs string, change templateStatusChange) output.Breadcrumb { + if change.state == "active" { + return output.Breadcrumb{ + Action: "duplicate", + Cmd: fmt.Sprintf("basecamp templates %s duplicate %d --in <project>%s", group, templateID, contextArgs), + Description: "Duplicate the template into a project", + } + } + return output.Breadcrumb{ + Action: "restore", + Cmd: fmt.Sprintf("basecamp templates %s restore %d%s", group, templateID, contextArgs), + Description: "Put the template back in the library", + } +} + +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 +630,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 +710,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 +721,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 +739,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 +768,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 +777,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 +833,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 +898,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 +976,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 +1021,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 +1093,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 +1160,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 +1173,217 @@ 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 + } + + // Unset stays off the wire so the server default applies, which also + // makes --copy-comments=false mean false rather than nothing. + req := &basecamp.CreateTemplatificationRequest{TemplateName: name} + if cmd.Flags().Changed("copy-comments") { + req.CopyComments = ©Comments + } + if cmd.Flags().Changed("copy-assignments") { + req.CopyAssignments = ©Assignments + } + if cmd.Flags().Changed("move-cards-to-triage") { + req.MoveCardsToTriage = &moveCardsToTriage + } + + save, err := app.Account().Templates().CreateTemplatification(cmd.Context(), bucketID, recordingID, req) + 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 397e11e52..e4c74f7e9 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, }) @@ -76,39 +76,38 @@ func TestTemplatesLibraryUsesSDKLibraryEndpoint(t *testing.T) { app.Config.ActiveProfile = "work profile" app.Config.Sources = map[string]string{"account_id": string(config.SourceFlag)} - err := executeRecordingCommand(NewTemplatesCmd(), app, "library") + err := executeRecordingCommand(NewTemplatesCmd(), app, "todolists", "list") require.NoError(t, err) 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, }) buf := &bytes.Buffer{} app.Output = output.New(output.Options{Format: output.FormatStyled, Writer: buf}) - err := executeRecordingCommand(NewTemplatesCmd(), app, "library") + err := executeRecordingCommand(NewTemplatesCmd(), app, "todolists", "list") require.NoError(t, err) assert.Contains(t, buf.String(), "Project kickoff") 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", @@ -120,21 +119,20 @@ func TestTemplatesCopyResolvesProjectAndTodosTool(t *testing.T) { app.Config.ActiveProfile = "work profile" app.Config.Sources = map[string]string{"account_id": string(config.SourceFlag)} - err := executeRecordingCommand(NewTemplatesCmd(), app, "copy", "3", "--in", "Test Project") + err := executeRecordingCommand(NewTemplatesCmd(), app, "todolists", "duplicate", "3", "--in", "Test Project") 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) { @@ -151,7 +149,7 @@ func TestTemplatesCopySendsExplicitPeopleConfirmation(t *testing.T) { ) err := executeRecordingCommand(NewTemplatesCmd(), app, - "copy", "3", "--project", "123", "--todoset", "9", "--confirm-adding-people") + "todolists", "duplicate", "3", "--project", "123", "--todoset", "9", "--confirm-adding-people") require.NoError(t, err) requests := transport.recorded() @@ -169,7 +167,7 @@ func TestTemplatesCopyRejectsTodosetFromAnotherProject(t *testing.T) { app, transport := setupRecordingTestApp(t, projectsRoute(), templateCopyTodosetRoute(456)) err := executeRecordingCommand(NewTemplatesCmd(), app, - "copy", "3", "--project", "123", "--todoset", "9") + "todolists", "duplicate", "3", "--project", "123", "--todoset", "9") require.Error(t, err) var outputErr *output.Error @@ -191,7 +189,7 @@ func TestTemplatesCopyRejectsDisabledTodoset(t *testing.T) { ) err := executeRecordingCommand(NewTemplatesCmd(), app, - "copy", "3", "--project", "123", "--todoset", "9") + "todolists", "duplicate", "3", "--project", "123", "--todoset", "9") require.Error(t, err) var outputErr *output.Error @@ -215,7 +213,7 @@ func TestTemplatesCopyExplainsPeopleConfirmation(t *testing.T) { app.Config.ActiveProfile = "work profile; echo unsafe" app.Config.Sources = map[string]string{"account_id": string(config.SourceFlag)} - err := executeRecordingCommand(NewTemplatesCmd(), app, "copy", "3", "--in", "123") + err := executeRecordingCommand(NewTemplatesCmd(), app, "todolists", "duplicate", "3", "--in", "123") require.Error(t, err) var outputErr *output.Error @@ -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") @@ -241,7 +239,7 @@ func TestTemplatesConstructSendsStartDateUnderProjectEnvelope(t *testing.T) { buf := captureTemplateOutput(app) err := executeRecordingCommand(NewTemplatesCmd(), app, - "construct", "2085958507", "--name", "Marketing Campaign", + "projects", "construct", "2085958507", "--name", "Marketing Campaign", "--description", "For Client: Xyz Corp Conference", "--start-date", "2026-09-01") require.NoError(t, err) @@ -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) { @@ -270,7 +268,7 @@ func TestTemplatesConstructOmitsStartDateWhenUnset(t *testing.T) { ) captureTemplateOutput(app) - err := executeRecordingCommand(NewTemplatesCmd(), app, "construct", "2085958507", "--name", "Marketing Campaign") + err := executeRecordingCommand(NewTemplatesCmd(), app, "projects", "construct", "2085958507", "--name", "Marketing Campaign") require.NoError(t, err) requests := transport.recorded() @@ -290,7 +288,7 @@ func TestTemplatesConstructParsesNaturalStartDate(t *testing.T) { captureTemplateOutput(app) before := time.Now() - err := executeRecordingCommand(NewTemplatesCmd(), app, "construct", "2085958507", "--name", "Marketing Campaign", "--start-date", "tomorrow") + err := executeRecordingCommand(NewTemplatesCmd(), app, "projects", "construct", "2085958507", "--name", "Marketing Campaign", "--start-date", "tomorrow") after := time.Now() require.NoError(t, err) @@ -310,7 +308,7 @@ func TestTemplatesConstructRejectsBlankStartDate(t *testing.T) { app, transport := setupRecordingTestApp(t) captureTemplateOutput(app) - err := executeRecordingCommand(NewTemplatesCmd(), app, "construct", "2085958507", "--name", "Marketing Campaign", "--start-date", "") + err := executeRecordingCommand(NewTemplatesCmd(), app, "projects", "construct", "2085958507", "--name", "Marketing Campaign", "--start-date", "") outErr := requireUsageErr(t, err) assert.Contains(t, outErr.Message, "Invalid start date") assert.Empty(t, transport.recorded()) @@ -320,7 +318,7 @@ func TestTemplatesConstructRejectsMalformedStartDateBeforeAnyRequest(t *testing. app, transport := setupRecordingTestApp(t) captureTemplateOutput(app) - err := executeRecordingCommand(NewTemplatesCmd(), app, "construct", "2085958507", "--name", "Marketing Campaign", "--start-date", "someday") + err := executeRecordingCommand(NewTemplatesCmd(), app, "projects", "construct", "2085958507", "--name", "Marketing Campaign", "--start-date", "someday") outErr := requireUsageErr(t, err) assert.Contains(t, outErr.Message, "Invalid start date") assert.Empty(t, transport.recorded()) @@ -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 { @@ -356,7 +354,7 @@ func TestTemplatesCopyStatusStates(t *testing.T) { app.Config.ActiveProfile = "work profile" app.Config.Sources = map[string]string{"account_id": string(config.SourceFlag)} - err := executeRecordingCommand(NewTemplatesCmd(), app, "copy-status", "5") + err := executeRecordingCommand(NewTemplatesCmd(), app, "todolists", "duplication", "5") require.NoError(t, err) request := transport.last(t) @@ -369,3 +367,245 @@ 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) +} + +func TestCardTablesTemplatifySendsMoveCardsToTriage(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"}`, + }, + ) + + err := executeRecordingCommand(NewCardTablesCmd(), app, + "templatify", "55", "--in", "123", "--move-cards-to-triage") + require.NoError(t, err) + + post := transport.recorded()[len(transport.recorded())-1] + assert.JSONEq(t, `{"move_cards_to_triage":true}`, post.Body) +} + +func TestCardTablesTemplatifyOmitsFlagsLeftUnset(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"}`, + }, + ) + + err := executeRecordingCommand(NewCardTablesCmd(), app, "templatify", "55", "--in", "123") + require.NoError(t, err) + + post := transport.recorded()[len(transport.recorded())-1] + assert.JSONEq(t, `{}`, post.Body, "an unset flag stays off the wire so the server default applies") +} + +func TestCardTablesTemplatificationReportsTheBoardItMade(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_card_table":{"id":70,"title":"Launch board","type":"Kanban::Board"}}`, + }, + ) + buf := captureTemplateOutput(app) + + err := executeRecordingCommand(NewCardTablesCmd(), app, "templatification", "55", "9", "--in", "123") + require.NoError(t, err) + + envelope := decodeTemplateEnvelope(t, buf) + assert.Equal(t, "Saved as template: Launch board (card table template #70)", envelope.Summary, + "a nil destination would fall through to the generic completion line") + require.Len(t, envelope.Breadcrumbs, 1) + assert.Equal(t, "basecamp templates card-tables list", envelope.Breadcrumbs[0].Cmd) +} + +func templateStatusRoute(status string) stubRoute { + return stubRoute{ + method: http.MethodPut, + path: fmt.Sprintf("/99999/recordings/3/status/%s.json", status), + status: http.StatusNoContent, + } +} + +func TestTemplatesCardTablesArchiveGoesThroughTheRecordingsEndpoint(t *testing.T) { + app, transport := setupRecordingTestApp(t, templateStatusRoute("archived")) + buf := captureTemplateOutput(app) + + err := executeRecordingCommand(NewTemplatesCmd(), app, "card-tables", "archive", "3") + require.NoError(t, err) + + request := transport.last(t) + assert.Equal(t, http.MethodPut, request.Method) + assert.Equal(t, "/99999/recordings/3/status/archived.json", request.Path, + "the project-template routes serve project templates only and 404 for a library template") + envelope := decodeTemplateEnvelope(t, buf) + assert.Equal(t, "Archived card table template #3", envelope.Summary) + require.Len(t, envelope.Breadcrumbs, 2) + assert.Equal(t, "basecamp templates card-tables restore 3", envelope.Breadcrumbs[1].Cmd) +} + +func TestTemplatesTodolistsTrashGoesThroughTheRecordingsEndpoint(t *testing.T) { + app, transport := setupRecordingTestApp(t, templateStatusRoute("trashed")) + buf := captureTemplateOutput(app) + + err := executeRecordingCommand(NewTemplatesCmd(), app, "todolists", "trash", "3") + require.NoError(t, err) + + assert.Equal(t, "/99999/recordings/3/status/trashed.json", transport.last(t).Path) + envelope := decodeTemplateEnvelope(t, buf) + assert.Equal(t, "Trashed to-do list template #3", envelope.Summary) + assert.Equal(t, "basecamp templates todolists restore 3", envelope.Breadcrumbs[1].Cmd) +} + +func TestTemplatesRestorePointsBackAtDuplicating(t *testing.T) { + app, transport := setupRecordingTestApp(t, templateStatusRoute("active")) + buf := captureTemplateOutput(app) + + err := executeRecordingCommand(NewTemplatesCmd(), app, "todolists", "restore", "3") + require.NoError(t, err) + + assert.Equal(t, "/99999/recordings/3/status/active.json", transport.last(t).Path) + envelope := decodeTemplateEnvelope(t, buf) + assert.Equal(t, "Restored to-do list template #3", envelope.Summary) + assert.Equal(t, "basecamp templates todolists duplicate 3 --in <project>", envelope.Breadcrumbs[1].Cmd) +} + +func TestTemplatesArchiveRejectsANonNumericID(t *testing.T) { + app, transport := setupRecordingTestApp(t) + + err := executeRecordingCommand(NewTemplatesCmd(), app, "card-tables", "archive", "kickoff") + require.Error(t, err) + + assert.Empty(t, transport.recorded(), "a bad ID is caught before any request goes out") +} diff --git a/internal/commands/todolists.go b/internal/commands/todolists.go index dbd20f635..edd2c7909 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 20a688290..6587c4c24 100644 --- a/skills/basecamp/SKILL.md +++ b/skills/basecamp/SKILL.md @@ -971,30 +971,80 @@ basecamp recordings visibility <id> --hidden # Hide from clients ### Templates -```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 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 -``` +Templates are grouped by the kind of template each verb operates on. + +```bash +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 todolists archive <template_id> # Take out of the library, keep it +basecamp templates todolists trash <template_id> # Move to the trash +basecamp templates todolists restore <template_id> # Put it back in the library + +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 +basecamp templates card-tables archive <template_id> +basecamp templates card-tables trash <template_id> +basecamp templates card-tables restore <template_id> +``` + +`archive`, `trash`, and `restore` are the library kinds' equivalent of +`templates projects delete`, which serves project templates only and 404s for a +to-do list or card table template. `list` shows active templates, so note the ID +before archiving or trashing one. + +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. + +**Every verb lives under its kind.** The flat pre-grouping spellings +(`templates list`, `templates show`, `templates create`, `templates update`, +`templates delete`, `templates construct`, `templates construction`, +`templates library`, `templates copy`, `templates copy-status`) have been +removed — use `templates projects delete`, not `templates delete`. `copy` and +`copy-status` still work inside `templates todolists` and +`templates card-tables` as aliases of `duplicate` and `duplication`. **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