diff --git a/README.md b/README.md index 773194e..0137af8 100644 --- a/README.md +++ b/README.md @@ -87,10 +87,14 @@ fizzy comment create --card 42 --body "Looks good!" # Add comment ### Output Formats ```bash -fizzy board list # JSON output -fizzy board list | jq '.data' # Pipe through jq for raw data +fizzy board list # JSON output +fizzy board list --jq '.data[0].name' # Filter the JSON envelope (built-in, no external jq required) +fizzy board list --quiet --jq '.[0].name' # Filter raw data without the envelope +fizzy board list --jq '[.data[] | {id, name}]' # Extract specific fields ``` +`--jq` is for machine-readable JSON output. It implies `--json` and cannot be combined with `--styled`, `--markdown`, `--ids-only`, or `--count`. + ### JSON Envelope Every command returns structured JSON: diff --git a/SURFACE.txt b/SURFACE.txt index 8021422..3bd5b44 100644 --- a/SURFACE.txt +++ b/SURFACE.txt @@ -155,6 +155,7 @@ FLAG fizzy --api-url type=string FLAG fizzy --count type=bool FLAG fizzy --help type=bool FLAG fizzy --ids-only type=bool +FLAG fizzy --jq type=string FLAG fizzy --json type=bool FLAG fizzy --limit type=int FLAG fizzy --markdown type=bool @@ -169,6 +170,7 @@ FLAG fizzy account --api-url type=string FLAG fizzy account --count type=bool FLAG fizzy account --help type=bool FLAG fizzy account --ids-only type=bool +FLAG fizzy account --jq type=string FLAG fizzy account --json type=bool FLAG fizzy account --limit type=int FLAG fizzy account --markdown type=bool @@ -183,6 +185,7 @@ FLAG fizzy account entropy --auto_postpone_period_in_days type=int FLAG fizzy account entropy --count type=bool FLAG fizzy account entropy --help type=bool FLAG fizzy account entropy --ids-only type=bool +FLAG fizzy account entropy --jq type=string FLAG fizzy account entropy --json type=bool FLAG fizzy account entropy --limit type=int FLAG fizzy account entropy --markdown type=bool @@ -196,6 +199,7 @@ FLAG fizzy account export-create --api-url type=string FLAG fizzy account export-create --count type=bool FLAG fizzy account export-create --help type=bool FLAG fizzy account export-create --ids-only type=bool +FLAG fizzy account export-create --jq type=string FLAG fizzy account export-create --json type=bool FLAG fizzy account export-create --limit type=int FLAG fizzy account export-create --markdown type=bool @@ -209,6 +213,7 @@ FLAG fizzy account export-show --api-url type=string FLAG fizzy account export-show --count type=bool FLAG fizzy account export-show --help type=bool FLAG fizzy account export-show --ids-only type=bool +FLAG fizzy account export-show --jq type=string FLAG fizzy account export-show --json type=bool FLAG fizzy account export-show --limit type=int FLAG fizzy account export-show --markdown type=bool @@ -222,6 +227,7 @@ FLAG fizzy account help --api-url type=string FLAG fizzy account help --count type=bool FLAG fizzy account help --help type=bool FLAG fizzy account help --ids-only type=bool +FLAG fizzy account help --jq type=string FLAG fizzy account help --json type=bool FLAG fizzy account help --limit type=int FLAG fizzy account help --markdown type=bool @@ -235,6 +241,7 @@ FLAG fizzy account join-code-reset --api-url type=string FLAG fizzy account join-code-reset --count type=bool FLAG fizzy account join-code-reset --help type=bool FLAG fizzy account join-code-reset --ids-only type=bool +FLAG fizzy account join-code-reset --jq type=string FLAG fizzy account join-code-reset --json type=bool FLAG fizzy account join-code-reset --limit type=int FLAG fizzy account join-code-reset --markdown type=bool @@ -248,6 +255,7 @@ FLAG fizzy account join-code-show --api-url type=string FLAG fizzy account join-code-show --count type=bool FLAG fizzy account join-code-show --help type=bool FLAG fizzy account join-code-show --ids-only type=bool +FLAG fizzy account join-code-show --jq type=string FLAG fizzy account join-code-show --json type=bool FLAG fizzy account join-code-show --limit type=int FLAG fizzy account join-code-show --markdown type=bool @@ -261,6 +269,7 @@ FLAG fizzy account join-code-update --api-url type=string FLAG fizzy account join-code-update --count type=bool FLAG fizzy account join-code-update --help type=bool FLAG fizzy account join-code-update --ids-only type=bool +FLAG fizzy account join-code-update --jq type=string FLAG fizzy account join-code-update --json type=bool FLAG fizzy account join-code-update --limit type=int FLAG fizzy account join-code-update --markdown type=bool @@ -275,6 +284,7 @@ FLAG fizzy account settings-update --api-url type=string FLAG fizzy account settings-update --count type=bool FLAG fizzy account settings-update --help type=bool FLAG fizzy account settings-update --ids-only type=bool +FLAG fizzy account settings-update --jq type=string FLAG fizzy account settings-update --json type=bool FLAG fizzy account settings-update --limit type=int FLAG fizzy account settings-update --markdown type=bool @@ -289,6 +299,7 @@ FLAG fizzy account show --api-url type=string FLAG fizzy account show --count type=bool FLAG fizzy account show --help type=bool FLAG fizzy account show --ids-only type=bool +FLAG fizzy account show --jq type=string FLAG fizzy account show --json type=bool FLAG fizzy account show --limit type=int FLAG fizzy account show --markdown type=bool @@ -302,6 +313,7 @@ FLAG fizzy auth --api-url type=string FLAG fizzy auth --count type=bool FLAG fizzy auth --help type=bool FLAG fizzy auth --ids-only type=bool +FLAG fizzy auth --jq type=string FLAG fizzy auth --json type=bool FLAG fizzy auth --limit type=int FLAG fizzy auth --markdown type=bool @@ -315,6 +327,7 @@ FLAG fizzy auth help --api-url type=string FLAG fizzy auth help --count type=bool FLAG fizzy auth help --help type=bool FLAG fizzy auth help --ids-only type=bool +FLAG fizzy auth help --jq type=string FLAG fizzy auth help --json type=bool FLAG fizzy auth help --limit type=int FLAG fizzy auth help --markdown type=bool @@ -328,6 +341,7 @@ FLAG fizzy auth list --api-url type=string FLAG fizzy auth list --count type=bool FLAG fizzy auth list --help type=bool FLAG fizzy auth list --ids-only type=bool +FLAG fizzy auth list --jq type=string FLAG fizzy auth list --json type=bool FLAG fizzy auth list --limit type=int FLAG fizzy auth list --markdown type=bool @@ -341,6 +355,7 @@ FLAG fizzy auth login --api-url type=string FLAG fizzy auth login --count type=bool FLAG fizzy auth login --help type=bool FLAG fizzy auth login --ids-only type=bool +FLAG fizzy auth login --jq type=string FLAG fizzy auth login --json type=bool FLAG fizzy auth login --limit type=int FLAG fizzy auth login --markdown type=bool @@ -355,6 +370,7 @@ FLAG fizzy auth logout --api-url type=string FLAG fizzy auth logout --count type=bool FLAG fizzy auth logout --help type=bool FLAG fizzy auth logout --ids-only type=bool +FLAG fizzy auth logout --jq type=string FLAG fizzy auth logout --json type=bool FLAG fizzy auth logout --limit type=int FLAG fizzy auth logout --markdown type=bool @@ -368,6 +384,7 @@ FLAG fizzy auth status --api-url type=string FLAG fizzy auth status --count type=bool FLAG fizzy auth status --help type=bool FLAG fizzy auth status --ids-only type=bool +FLAG fizzy auth status --jq type=string FLAG fizzy auth status --json type=bool FLAG fizzy auth status --limit type=int FLAG fizzy auth status --markdown type=bool @@ -381,6 +398,7 @@ FLAG fizzy auth switch --api-url type=string FLAG fizzy auth switch --count type=bool FLAG fizzy auth switch --help type=bool FLAG fizzy auth switch --ids-only type=bool +FLAG fizzy auth switch --jq type=string FLAG fizzy auth switch --json type=bool FLAG fizzy auth switch --limit type=int FLAG fizzy auth switch --markdown type=bool @@ -394,6 +412,7 @@ FLAG fizzy board --api-url type=string FLAG fizzy board --count type=bool FLAG fizzy board --help type=bool FLAG fizzy board --ids-only type=bool +FLAG fizzy board --jq type=string FLAG fizzy board --json type=bool FLAG fizzy board --limit type=int FLAG fizzy board --markdown type=bool @@ -409,6 +428,7 @@ FLAG fizzy board closed --board type=string FLAG fizzy board closed --count type=bool FLAG fizzy board closed --help type=bool FLAG fizzy board closed --ids-only type=bool +FLAG fizzy board closed --jq type=string FLAG fizzy board closed --json type=bool FLAG fizzy board closed --limit type=int FLAG fizzy board closed --markdown type=bool @@ -425,6 +445,7 @@ FLAG fizzy board create --auto_postpone_period_in_days type=int FLAG fizzy board create --count type=bool FLAG fizzy board create --help type=bool FLAG fizzy board create --ids-only type=bool +FLAG fizzy board create --jq type=string FLAG fizzy board create --json type=bool FLAG fizzy board create --limit type=int FLAG fizzy board create --markdown type=bool @@ -439,6 +460,7 @@ FLAG fizzy board delete --api-url type=string FLAG fizzy board delete --count type=bool FLAG fizzy board delete --help type=bool FLAG fizzy board delete --ids-only type=bool +FLAG fizzy board delete --jq type=string FLAG fizzy board delete --json type=bool FLAG fizzy board delete --limit type=int FLAG fizzy board delete --markdown type=bool @@ -453,6 +475,7 @@ FLAG fizzy board entropy --auto_postpone_period_in_days type=int FLAG fizzy board entropy --count type=bool FLAG fizzy board entropy --help type=bool FLAG fizzy board entropy --ids-only type=bool +FLAG fizzy board entropy --jq type=string FLAG fizzy board entropy --json type=bool FLAG fizzy board entropy --limit type=int FLAG fizzy board entropy --markdown type=bool @@ -466,6 +489,7 @@ FLAG fizzy board help --api-url type=string FLAG fizzy board help --count type=bool FLAG fizzy board help --help type=bool FLAG fizzy board help --ids-only type=bool +FLAG fizzy board help --jq type=string FLAG fizzy board help --json type=bool FLAG fizzy board help --limit type=int FLAG fizzy board help --markdown type=bool @@ -480,6 +504,7 @@ FLAG fizzy board involvement --count type=bool FLAG fizzy board involvement --help type=bool FLAG fizzy board involvement --ids-only type=bool FLAG fizzy board involvement --involvement type=string +FLAG fizzy board involvement --jq type=string FLAG fizzy board involvement --json type=bool FLAG fizzy board involvement --limit type=int FLAG fizzy board involvement --markdown type=bool @@ -494,6 +519,7 @@ FLAG fizzy board list --api-url type=string FLAG fizzy board list --count type=bool FLAG fizzy board list --help type=bool FLAG fizzy board list --ids-only type=bool +FLAG fizzy board list --jq type=string FLAG fizzy board list --json type=bool FLAG fizzy board list --limit type=int FLAG fizzy board list --markdown type=bool @@ -510,6 +536,7 @@ FLAG fizzy board postponed --board type=string FLAG fizzy board postponed --count type=bool FLAG fizzy board postponed --help type=bool FLAG fizzy board postponed --ids-only type=bool +FLAG fizzy board postponed --jq type=string FLAG fizzy board postponed --json type=bool FLAG fizzy board postponed --limit type=int FLAG fizzy board postponed --markdown type=bool @@ -524,6 +551,7 @@ FLAG fizzy board publish --api-url type=string FLAG fizzy board publish --count type=bool FLAG fizzy board publish --help type=bool FLAG fizzy board publish --ids-only type=bool +FLAG fizzy board publish --jq type=string FLAG fizzy board publish --json type=bool FLAG fizzy board publish --limit type=int FLAG fizzy board publish --markdown type=bool @@ -537,6 +565,7 @@ FLAG fizzy board show --api-url type=string FLAG fizzy board show --count type=bool FLAG fizzy board show --help type=bool FLAG fizzy board show --ids-only type=bool +FLAG fizzy board show --jq type=string FLAG fizzy board show --json type=bool FLAG fizzy board show --limit type=int FLAG fizzy board show --markdown type=bool @@ -552,6 +581,7 @@ FLAG fizzy board stream --board type=string FLAG fizzy board stream --count type=bool FLAG fizzy board stream --help type=bool FLAG fizzy board stream --ids-only type=bool +FLAG fizzy board stream --jq type=string FLAG fizzy board stream --json type=bool FLAG fizzy board stream --limit type=int FLAG fizzy board stream --markdown type=bool @@ -566,6 +596,7 @@ FLAG fizzy board unpublish --api-url type=string FLAG fizzy board unpublish --count type=bool FLAG fizzy board unpublish --help type=bool FLAG fizzy board unpublish --ids-only type=bool +FLAG fizzy board unpublish --jq type=string FLAG fizzy board unpublish --json type=bool FLAG fizzy board unpublish --limit type=int FLAG fizzy board unpublish --markdown type=bool @@ -581,6 +612,7 @@ FLAG fizzy board update --auto_postpone_period_in_days type=int FLAG fizzy board update --count type=bool FLAG fizzy board update --help type=bool FLAG fizzy board update --ids-only type=bool +FLAG fizzy board update --jq type=string FLAG fizzy board update --json type=bool FLAG fizzy board update --limit type=int FLAG fizzy board update --markdown type=bool @@ -595,6 +627,7 @@ FLAG fizzy card --api-url type=string FLAG fizzy card --count type=bool FLAG fizzy card --help type=bool FLAG fizzy card --ids-only type=bool +FLAG fizzy card --jq type=string FLAG fizzy card --json type=bool FLAG fizzy card --limit type=int FLAG fizzy card --markdown type=bool @@ -608,6 +641,7 @@ FLAG fizzy card assign --api-url type=string FLAG fizzy card assign --count type=bool FLAG fizzy card assign --help type=bool FLAG fizzy card assign --ids-only type=bool +FLAG fizzy card assign --jq type=string FLAG fizzy card assign --json type=bool FLAG fizzy card assign --limit type=int FLAG fizzy card assign --markdown type=bool @@ -622,6 +656,7 @@ FLAG fizzy card attachments --api-url type=string FLAG fizzy card attachments --count type=bool FLAG fizzy card attachments --help type=bool FLAG fizzy card attachments --ids-only type=bool +FLAG fizzy card attachments --jq type=string FLAG fizzy card attachments --json type=bool FLAG fizzy card attachments --limit type=int FLAG fizzy card attachments --markdown type=bool @@ -636,6 +671,7 @@ FLAG fizzy card attachments download --count type=bool FLAG fizzy card attachments download --help type=bool FLAG fizzy card attachments download --ids-only type=bool FLAG fizzy card attachments download --include-comments type=bool +FLAG fizzy card attachments download --jq type=string FLAG fizzy card attachments download --json type=bool FLAG fizzy card attachments download --limit type=int FLAG fizzy card attachments download --markdown type=bool @@ -650,6 +686,7 @@ FLAG fizzy card attachments help --api-url type=string FLAG fizzy card attachments help --count type=bool FLAG fizzy card attachments help --help type=bool FLAG fizzy card attachments help --ids-only type=bool +FLAG fizzy card attachments help --jq type=string FLAG fizzy card attachments help --json type=bool FLAG fizzy card attachments help --limit type=int FLAG fizzy card attachments help --markdown type=bool @@ -664,6 +701,7 @@ FLAG fizzy card attachments show --count type=bool FLAG fizzy card attachments show --help type=bool FLAG fizzy card attachments show --ids-only type=bool FLAG fizzy card attachments show --include-comments type=bool +FLAG fizzy card attachments show --jq type=string FLAG fizzy card attachments show --json type=bool FLAG fizzy card attachments show --limit type=int FLAG fizzy card attachments show --markdown type=bool @@ -677,6 +715,7 @@ FLAG fizzy card close --api-url type=string FLAG fizzy card close --count type=bool FLAG fizzy card close --help type=bool FLAG fizzy card close --ids-only type=bool +FLAG fizzy card close --jq type=string FLAG fizzy card close --json type=bool FLAG fizzy card close --limit type=int FLAG fizzy card close --markdown type=bool @@ -691,6 +730,7 @@ FLAG fizzy card column --column type=string FLAG fizzy card column --count type=bool FLAG fizzy card column --help type=bool FLAG fizzy card column --ids-only type=bool +FLAG fizzy card column --jq type=string FLAG fizzy card column --json type=bool FLAG fizzy card column --limit type=int FLAG fizzy card column --markdown type=bool @@ -709,6 +749,7 @@ FLAG fizzy card create --description_file type=string FLAG fizzy card create --help type=bool FLAG fizzy card create --ids-only type=bool FLAG fizzy card create --image type=string +FLAG fizzy card create --jq type=string FLAG fizzy card create --json type=bool FLAG fizzy card create --limit type=int FLAG fizzy card create --markdown type=bool @@ -723,6 +764,7 @@ FLAG fizzy card delete --api-url type=string FLAG fizzy card delete --count type=bool FLAG fizzy card delete --help type=bool FLAG fizzy card delete --ids-only type=bool +FLAG fizzy card delete --jq type=string FLAG fizzy card delete --json type=bool FLAG fizzy card delete --limit type=int FLAG fizzy card delete --markdown type=bool @@ -736,6 +778,7 @@ FLAG fizzy card golden --api-url type=string FLAG fizzy card golden --count type=bool FLAG fizzy card golden --help type=bool FLAG fizzy card golden --ids-only type=bool +FLAG fizzy card golden --jq type=string FLAG fizzy card golden --json type=bool FLAG fizzy card golden --limit type=int FLAG fizzy card golden --markdown type=bool @@ -749,6 +792,7 @@ FLAG fizzy card help --api-url type=string FLAG fizzy card help --count type=bool FLAG fizzy card help --help type=bool FLAG fizzy card help --ids-only type=bool +FLAG fizzy card help --jq type=string FLAG fizzy card help --json type=bool FLAG fizzy card help --limit type=int FLAG fizzy card help --markdown type=bool @@ -762,6 +806,7 @@ FLAG fizzy card image-remove --api-url type=string FLAG fizzy card image-remove --count type=bool FLAG fizzy card image-remove --help type=bool FLAG fizzy card image-remove --ids-only type=bool +FLAG fizzy card image-remove --jq type=string FLAG fizzy card image-remove --json type=bool FLAG fizzy card image-remove --limit type=int FLAG fizzy card image-remove --markdown type=bool @@ -784,6 +829,7 @@ FLAG fizzy card list --creator type=string FLAG fizzy card list --help type=bool FLAG fizzy card list --ids-only type=bool FLAG fizzy card list --indexed-by type=string +FLAG fizzy card list --jq type=string FLAG fizzy card list --json type=bool FLAG fizzy card list --limit type=int FLAG fizzy card list --markdown type=bool @@ -802,6 +848,7 @@ FLAG fizzy card mark-read --api-url type=string FLAG fizzy card mark-read --count type=bool FLAG fizzy card mark-read --help type=bool FLAG fizzy card mark-read --ids-only type=bool +FLAG fizzy card mark-read --jq type=string FLAG fizzy card mark-read --json type=bool FLAG fizzy card mark-read --limit type=int FLAG fizzy card mark-read --markdown type=bool @@ -815,6 +862,7 @@ FLAG fizzy card mark-unread --api-url type=string FLAG fizzy card mark-unread --count type=bool FLAG fizzy card mark-unread --help type=bool FLAG fizzy card mark-unread --ids-only type=bool +FLAG fizzy card mark-unread --jq type=string FLAG fizzy card mark-unread --json type=bool FLAG fizzy card mark-unread --limit type=int FLAG fizzy card mark-unread --markdown type=bool @@ -828,6 +876,7 @@ FLAG fizzy card move --api-url type=string FLAG fizzy card move --count type=bool FLAG fizzy card move --help type=bool FLAG fizzy card move --ids-only type=bool +FLAG fizzy card move --jq type=string FLAG fizzy card move --json type=bool FLAG fizzy card move --limit type=int FLAG fizzy card move --markdown type=bool @@ -842,6 +891,7 @@ FLAG fizzy card pin --api-url type=string FLAG fizzy card pin --count type=bool FLAG fizzy card pin --help type=bool FLAG fizzy card pin --ids-only type=bool +FLAG fizzy card pin --jq type=string FLAG fizzy card pin --json type=bool FLAG fizzy card pin --limit type=int FLAG fizzy card pin --markdown type=bool @@ -855,6 +905,7 @@ FLAG fizzy card postpone --api-url type=string FLAG fizzy card postpone --count type=bool FLAG fizzy card postpone --help type=bool FLAG fizzy card postpone --ids-only type=bool +FLAG fizzy card postpone --jq type=string FLAG fizzy card postpone --json type=bool FLAG fizzy card postpone --limit type=int FLAG fizzy card postpone --markdown type=bool @@ -868,6 +919,7 @@ FLAG fizzy card publish --api-url type=string FLAG fizzy card publish --count type=bool FLAG fizzy card publish --help type=bool FLAG fizzy card publish --ids-only type=bool +FLAG fizzy card publish --jq type=string FLAG fizzy card publish --json type=bool FLAG fizzy card publish --limit type=int FLAG fizzy card publish --markdown type=bool @@ -881,6 +933,7 @@ FLAG fizzy card reopen --api-url type=string FLAG fizzy card reopen --count type=bool FLAG fizzy card reopen --help type=bool FLAG fizzy card reopen --ids-only type=bool +FLAG fizzy card reopen --jq type=string FLAG fizzy card reopen --json type=bool FLAG fizzy card reopen --limit type=int FLAG fizzy card reopen --markdown type=bool @@ -894,6 +947,7 @@ FLAG fizzy card self-assign --api-url type=string FLAG fizzy card self-assign --count type=bool FLAG fizzy card self-assign --help type=bool FLAG fizzy card self-assign --ids-only type=bool +FLAG fizzy card self-assign --jq type=string FLAG fizzy card self-assign --json type=bool FLAG fizzy card self-assign --limit type=int FLAG fizzy card self-assign --markdown type=bool @@ -907,6 +961,7 @@ FLAG fizzy card show --api-url type=string FLAG fizzy card show --count type=bool FLAG fizzy card show --help type=bool FLAG fizzy card show --ids-only type=bool +FLAG fizzy card show --jq type=string FLAG fizzy card show --json type=bool FLAG fizzy card show --limit type=int FLAG fizzy card show --markdown type=bool @@ -920,6 +975,7 @@ FLAG fizzy card tag --api-url type=string FLAG fizzy card tag --count type=bool FLAG fizzy card tag --help type=bool FLAG fizzy card tag --ids-only type=bool +FLAG fizzy card tag --jq type=string FLAG fizzy card tag --json type=bool FLAG fizzy card tag --limit type=int FLAG fizzy card tag --markdown type=bool @@ -934,6 +990,7 @@ FLAG fizzy card ungolden --api-url type=string FLAG fizzy card ungolden --count type=bool FLAG fizzy card ungolden --help type=bool FLAG fizzy card ungolden --ids-only type=bool +FLAG fizzy card ungolden --jq type=string FLAG fizzy card ungolden --json type=bool FLAG fizzy card ungolden --limit type=int FLAG fizzy card ungolden --markdown type=bool @@ -947,6 +1004,7 @@ FLAG fizzy card unpin --api-url type=string FLAG fizzy card unpin --count type=bool FLAG fizzy card unpin --help type=bool FLAG fizzy card unpin --ids-only type=bool +FLAG fizzy card unpin --jq type=string FLAG fizzy card unpin --json type=bool FLAG fizzy card unpin --limit type=int FLAG fizzy card unpin --markdown type=bool @@ -960,6 +1018,7 @@ FLAG fizzy card untriage --api-url type=string FLAG fizzy card untriage --count type=bool FLAG fizzy card untriage --help type=bool FLAG fizzy card untriage --ids-only type=bool +FLAG fizzy card untriage --jq type=string FLAG fizzy card untriage --json type=bool FLAG fizzy card untriage --limit type=int FLAG fizzy card untriage --markdown type=bool @@ -973,6 +1032,7 @@ FLAG fizzy card unwatch --api-url type=string FLAG fizzy card unwatch --count type=bool FLAG fizzy card unwatch --help type=bool FLAG fizzy card unwatch --ids-only type=bool +FLAG fizzy card unwatch --jq type=string FLAG fizzy card unwatch --json type=bool FLAG fizzy card unwatch --limit type=int FLAG fizzy card unwatch --markdown type=bool @@ -990,6 +1050,7 @@ FLAG fizzy card update --description_file type=string FLAG fizzy card update --help type=bool FLAG fizzy card update --ids-only type=bool FLAG fizzy card update --image type=string +FLAG fizzy card update --jq type=string FLAG fizzy card update --json type=bool FLAG fizzy card update --limit type=int FLAG fizzy card update --markdown type=bool @@ -1004,6 +1065,7 @@ FLAG fizzy card watch --api-url type=string FLAG fizzy card watch --count type=bool FLAG fizzy card watch --help type=bool FLAG fizzy card watch --ids-only type=bool +FLAG fizzy card watch --jq type=string FLAG fizzy card watch --json type=bool FLAG fizzy card watch --limit type=int FLAG fizzy card watch --markdown type=bool @@ -1017,6 +1079,7 @@ FLAG fizzy column --api-url type=string FLAG fizzy column --count type=bool FLAG fizzy column --help type=bool FLAG fizzy column --ids-only type=bool +FLAG fizzy column --jq type=string FLAG fizzy column --json type=bool FLAG fizzy column --limit type=int FLAG fizzy column --markdown type=bool @@ -1032,6 +1095,7 @@ FLAG fizzy column create --color type=string FLAG fizzy column create --count type=bool FLAG fizzy column create --help type=bool FLAG fizzy column create --ids-only type=bool +FLAG fizzy column create --jq type=string FLAG fizzy column create --json type=bool FLAG fizzy column create --limit type=int FLAG fizzy column create --markdown type=bool @@ -1047,6 +1111,7 @@ FLAG fizzy column delete --board type=string FLAG fizzy column delete --count type=bool FLAG fizzy column delete --help type=bool FLAG fizzy column delete --ids-only type=bool +FLAG fizzy column delete --jq type=string FLAG fizzy column delete --json type=bool FLAG fizzy column delete --limit type=int FLAG fizzy column delete --markdown type=bool @@ -1060,6 +1125,7 @@ FLAG fizzy column help --api-url type=string FLAG fizzy column help --count type=bool FLAG fizzy column help --help type=bool FLAG fizzy column help --ids-only type=bool +FLAG fizzy column help --jq type=string FLAG fizzy column help --json type=bool FLAG fizzy column help --limit type=int FLAG fizzy column help --markdown type=bool @@ -1074,6 +1140,7 @@ FLAG fizzy column list --board type=string FLAG fizzy column list --count type=bool FLAG fizzy column list --help type=bool FLAG fizzy column list --ids-only type=bool +FLAG fizzy column list --jq type=string FLAG fizzy column list --json type=bool FLAG fizzy column list --limit type=int FLAG fizzy column list --markdown type=bool @@ -1087,6 +1154,7 @@ FLAG fizzy column move-left --api-url type=string FLAG fizzy column move-left --count type=bool FLAG fizzy column move-left --help type=bool FLAG fizzy column move-left --ids-only type=bool +FLAG fizzy column move-left --jq type=string FLAG fizzy column move-left --json type=bool FLAG fizzy column move-left --limit type=int FLAG fizzy column move-left --markdown type=bool @@ -1100,6 +1168,7 @@ FLAG fizzy column move-right --api-url type=string FLAG fizzy column move-right --count type=bool FLAG fizzy column move-right --help type=bool FLAG fizzy column move-right --ids-only type=bool +FLAG fizzy column move-right --jq type=string FLAG fizzy column move-right --json type=bool FLAG fizzy column move-right --limit type=int FLAG fizzy column move-right --markdown type=bool @@ -1114,6 +1183,7 @@ FLAG fizzy column show --board type=string FLAG fizzy column show --count type=bool FLAG fizzy column show --help type=bool FLAG fizzy column show --ids-only type=bool +FLAG fizzy column show --jq type=string FLAG fizzy column show --json type=bool FLAG fizzy column show --limit type=int FLAG fizzy column show --markdown type=bool @@ -1129,6 +1199,7 @@ FLAG fizzy column update --color type=string FLAG fizzy column update --count type=bool FLAG fizzy column update --help type=bool FLAG fizzy column update --ids-only type=bool +FLAG fizzy column update --jq type=string FLAG fizzy column update --json type=bool FLAG fizzy column update --limit type=int FLAG fizzy column update --markdown type=bool @@ -1143,6 +1214,7 @@ FLAG fizzy commands --api-url type=string FLAG fizzy commands --count type=bool FLAG fizzy commands --help type=bool FLAG fizzy commands --ids-only type=bool +FLAG fizzy commands --jq type=string FLAG fizzy commands --json type=bool FLAG fizzy commands --limit type=int FLAG fizzy commands --markdown type=bool @@ -1156,6 +1228,7 @@ FLAG fizzy comment --api-url type=string FLAG fizzy comment --count type=bool FLAG fizzy comment --help type=bool FLAG fizzy comment --ids-only type=bool +FLAG fizzy comment --jq type=string FLAG fizzy comment --json type=bool FLAG fizzy comment --limit type=int FLAG fizzy comment --markdown type=bool @@ -1169,6 +1242,7 @@ FLAG fizzy comment attachments --api-url type=string FLAG fizzy comment attachments --count type=bool FLAG fizzy comment attachments --help type=bool FLAG fizzy comment attachments --ids-only type=bool +FLAG fizzy comment attachments --jq type=string FLAG fizzy comment attachments --json type=bool FLAG fizzy comment attachments --limit type=int FLAG fizzy comment attachments --markdown type=bool @@ -1183,6 +1257,7 @@ FLAG fizzy comment attachments download --card type=string FLAG fizzy comment attachments download --count type=bool FLAG fizzy comment attachments download --help type=bool FLAG fizzy comment attachments download --ids-only type=bool +FLAG fizzy comment attachments download --jq type=string FLAG fizzy comment attachments download --json type=bool FLAG fizzy comment attachments download --limit type=int FLAG fizzy comment attachments download --markdown type=bool @@ -1197,6 +1272,7 @@ FLAG fizzy comment attachments help --api-url type=string FLAG fizzy comment attachments help --count type=bool FLAG fizzy comment attachments help --help type=bool FLAG fizzy comment attachments help --ids-only type=bool +FLAG fizzy comment attachments help --jq type=string FLAG fizzy comment attachments help --json type=bool FLAG fizzy comment attachments help --limit type=int FLAG fizzy comment attachments help --markdown type=bool @@ -1211,6 +1287,7 @@ FLAG fizzy comment attachments show --card type=string FLAG fizzy comment attachments show --count type=bool FLAG fizzy comment attachments show --help type=bool FLAG fizzy comment attachments show --ids-only type=bool +FLAG fizzy comment attachments show --jq type=string FLAG fizzy comment attachments show --json type=bool FLAG fizzy comment attachments show --limit type=int FLAG fizzy comment attachments show --markdown type=bool @@ -1228,6 +1305,7 @@ FLAG fizzy comment create --count type=bool FLAG fizzy comment create --created-at type=string FLAG fizzy comment create --help type=bool FLAG fizzy comment create --ids-only type=bool +FLAG fizzy comment create --jq type=string FLAG fizzy comment create --json type=bool FLAG fizzy comment create --limit type=int FLAG fizzy comment create --markdown type=bool @@ -1242,6 +1320,7 @@ FLAG fizzy comment delete --card type=string FLAG fizzy comment delete --count type=bool FLAG fizzy comment delete --help type=bool FLAG fizzy comment delete --ids-only type=bool +FLAG fizzy comment delete --jq type=string FLAG fizzy comment delete --json type=bool FLAG fizzy comment delete --limit type=int FLAG fizzy comment delete --markdown type=bool @@ -1255,6 +1334,7 @@ FLAG fizzy comment help --api-url type=string FLAG fizzy comment help --count type=bool FLAG fizzy comment help --help type=bool FLAG fizzy comment help --ids-only type=bool +FLAG fizzy comment help --jq type=string FLAG fizzy comment help --json type=bool FLAG fizzy comment help --limit type=int FLAG fizzy comment help --markdown type=bool @@ -1270,6 +1350,7 @@ FLAG fizzy comment list --card type=string FLAG fizzy comment list --count type=bool FLAG fizzy comment list --help type=bool FLAG fizzy comment list --ids-only type=bool +FLAG fizzy comment list --jq type=string FLAG fizzy comment list --json type=bool FLAG fizzy comment list --limit type=int FLAG fizzy comment list --markdown type=bool @@ -1285,6 +1366,7 @@ FLAG fizzy comment show --card type=string FLAG fizzy comment show --count type=bool FLAG fizzy comment show --help type=bool FLAG fizzy comment show --ids-only type=bool +FLAG fizzy comment show --jq type=string FLAG fizzy comment show --json type=bool FLAG fizzy comment show --limit type=int FLAG fizzy comment show --markdown type=bool @@ -1301,6 +1383,7 @@ FLAG fizzy comment update --card type=string FLAG fizzy comment update --count type=bool FLAG fizzy comment update --help type=bool FLAG fizzy comment update --ids-only type=bool +FLAG fizzy comment update --jq type=string FLAG fizzy comment update --json type=bool FLAG fizzy comment update --limit type=int FLAG fizzy comment update --markdown type=bool @@ -1314,6 +1397,7 @@ FLAG fizzy completion --api-url type=string FLAG fizzy completion --count type=bool FLAG fizzy completion --help type=bool FLAG fizzy completion --ids-only type=bool +FLAG fizzy completion --jq type=string FLAG fizzy completion --json type=bool FLAG fizzy completion --limit type=int FLAG fizzy completion --markdown type=bool @@ -1327,6 +1411,7 @@ FLAG fizzy help --api-url type=string FLAG fizzy help --count type=bool FLAG fizzy help --help type=bool FLAG fizzy help --ids-only type=bool +FLAG fizzy help --jq type=string FLAG fizzy help --json type=bool FLAG fizzy help --limit type=int FLAG fizzy help --markdown type=bool @@ -1340,6 +1425,7 @@ FLAG fizzy identity --api-url type=string FLAG fizzy identity --count type=bool FLAG fizzy identity --help type=bool FLAG fizzy identity --ids-only type=bool +FLAG fizzy identity --jq type=string FLAG fizzy identity --json type=bool FLAG fizzy identity --limit type=int FLAG fizzy identity --markdown type=bool @@ -1353,6 +1439,7 @@ FLAG fizzy identity help --api-url type=string FLAG fizzy identity help --count type=bool FLAG fizzy identity help --help type=bool FLAG fizzy identity help --ids-only type=bool +FLAG fizzy identity help --jq type=string FLAG fizzy identity help --json type=bool FLAG fizzy identity help --limit type=int FLAG fizzy identity help --markdown type=bool @@ -1366,6 +1453,7 @@ FLAG fizzy identity show --api-url type=string FLAG fizzy identity show --count type=bool FLAG fizzy identity show --help type=bool FLAG fizzy identity show --ids-only type=bool +FLAG fizzy identity show --jq type=string FLAG fizzy identity show --json type=bool FLAG fizzy identity show --limit type=int FLAG fizzy identity show --markdown type=bool @@ -1379,6 +1467,7 @@ FLAG fizzy migrate --api-url type=string FLAG fizzy migrate --count type=bool FLAG fizzy migrate --help type=bool FLAG fizzy migrate --ids-only type=bool +FLAG fizzy migrate --jq type=string FLAG fizzy migrate --json type=bool FLAG fizzy migrate --limit type=int FLAG fizzy migrate --markdown type=bool @@ -1397,6 +1486,7 @@ FLAG fizzy migrate board --ids-only type=bool FLAG fizzy migrate board --include-comments type=bool FLAG fizzy migrate board --include-images type=bool FLAG fizzy migrate board --include-steps type=bool +FLAG fizzy migrate board --jq type=string FLAG fizzy migrate board --json type=bool FLAG fizzy migrate board --limit type=int FLAG fizzy migrate board --markdown type=bool @@ -1411,6 +1501,7 @@ FLAG fizzy migrate help --api-url type=string FLAG fizzy migrate help --count type=bool FLAG fizzy migrate help --help type=bool FLAG fizzy migrate help --ids-only type=bool +FLAG fizzy migrate help --jq type=string FLAG fizzy migrate help --json type=bool FLAG fizzy migrate help --limit type=int FLAG fizzy migrate help --markdown type=bool @@ -1424,6 +1515,7 @@ FLAG fizzy notification --api-url type=string FLAG fizzy notification --count type=bool FLAG fizzy notification --help type=bool FLAG fizzy notification --ids-only type=bool +FLAG fizzy notification --jq type=string FLAG fizzy notification --json type=bool FLAG fizzy notification --limit type=int FLAG fizzy notification --markdown type=bool @@ -1437,6 +1529,7 @@ FLAG fizzy notification help --api-url type=string FLAG fizzy notification help --count type=bool FLAG fizzy notification help --help type=bool FLAG fizzy notification help --ids-only type=bool +FLAG fizzy notification help --jq type=string FLAG fizzy notification help --json type=bool FLAG fizzy notification help --limit type=int FLAG fizzy notification help --markdown type=bool @@ -1451,6 +1544,7 @@ FLAG fizzy notification list --api-url type=string FLAG fizzy notification list --count type=bool FLAG fizzy notification list --help type=bool FLAG fizzy notification list --ids-only type=bool +FLAG fizzy notification list --jq type=string FLAG fizzy notification list --json type=bool FLAG fizzy notification list --limit type=int FLAG fizzy notification list --markdown type=bool @@ -1465,6 +1559,7 @@ FLAG fizzy notification read --api-url type=string FLAG fizzy notification read --count type=bool FLAG fizzy notification read --help type=bool FLAG fizzy notification read --ids-only type=bool +FLAG fizzy notification read --jq type=string FLAG fizzy notification read --json type=bool FLAG fizzy notification read --limit type=int FLAG fizzy notification read --markdown type=bool @@ -1478,6 +1573,7 @@ FLAG fizzy notification read-all --api-url type=string FLAG fizzy notification read-all --count type=bool FLAG fizzy notification read-all --help type=bool FLAG fizzy notification read-all --ids-only type=bool +FLAG fizzy notification read-all --jq type=string FLAG fizzy notification read-all --json type=bool FLAG fizzy notification read-all --limit type=int FLAG fizzy notification read-all --markdown type=bool @@ -1491,6 +1587,7 @@ FLAG fizzy notification settings-show --api-url type=string FLAG fizzy notification settings-show --count type=bool FLAG fizzy notification settings-show --help type=bool FLAG fizzy notification settings-show --ids-only type=bool +FLAG fizzy notification settings-show --jq type=string FLAG fizzy notification settings-show --json type=bool FLAG fizzy notification settings-show --limit type=int FLAG fizzy notification settings-show --markdown type=bool @@ -1505,6 +1602,7 @@ FLAG fizzy notification settings-update --bundle-email-frequency type=string FLAG fizzy notification settings-update --count type=bool FLAG fizzy notification settings-update --help type=bool FLAG fizzy notification settings-update --ids-only type=bool +FLAG fizzy notification settings-update --jq type=string FLAG fizzy notification settings-update --json type=bool FLAG fizzy notification settings-update --limit type=int FLAG fizzy notification settings-update --markdown type=bool @@ -1519,6 +1617,7 @@ FLAG fizzy notification tray --count type=bool FLAG fizzy notification tray --help type=bool FLAG fizzy notification tray --ids-only type=bool FLAG fizzy notification tray --include-read type=bool +FLAG fizzy notification tray --jq type=string FLAG fizzy notification tray --json type=bool FLAG fizzy notification tray --limit type=int FLAG fizzy notification tray --markdown type=bool @@ -1532,6 +1631,7 @@ FLAG fizzy notification unread --api-url type=string FLAG fizzy notification unread --count type=bool FLAG fizzy notification unread --help type=bool FLAG fizzy notification unread --ids-only type=bool +FLAG fizzy notification unread --jq type=string FLAG fizzy notification unread --json type=bool FLAG fizzy notification unread --limit type=int FLAG fizzy notification unread --markdown type=bool @@ -1545,6 +1645,7 @@ FLAG fizzy pin --api-url type=string FLAG fizzy pin --count type=bool FLAG fizzy pin --help type=bool FLAG fizzy pin --ids-only type=bool +FLAG fizzy pin --jq type=string FLAG fizzy pin --json type=bool FLAG fizzy pin --limit type=int FLAG fizzy pin --markdown type=bool @@ -1558,6 +1659,7 @@ FLAG fizzy pin help --api-url type=string FLAG fizzy pin help --count type=bool FLAG fizzy pin help --help type=bool FLAG fizzy pin help --ids-only type=bool +FLAG fizzy pin help --jq type=string FLAG fizzy pin help --json type=bool FLAG fizzy pin help --limit type=int FLAG fizzy pin help --markdown type=bool @@ -1571,6 +1673,7 @@ FLAG fizzy pin list --api-url type=string FLAG fizzy pin list --count type=bool FLAG fizzy pin list --help type=bool FLAG fizzy pin list --ids-only type=bool +FLAG fizzy pin list --jq type=string FLAG fizzy pin list --json type=bool FLAG fizzy pin list --limit type=int FLAG fizzy pin list --markdown type=bool @@ -1584,6 +1687,7 @@ FLAG fizzy reaction --api-url type=string FLAG fizzy reaction --count type=bool FLAG fizzy reaction --help type=bool FLAG fizzy reaction --ids-only type=bool +FLAG fizzy reaction --jq type=string FLAG fizzy reaction --json type=bool FLAG fizzy reaction --limit type=int FLAG fizzy reaction --markdown type=bool @@ -1600,6 +1704,7 @@ FLAG fizzy reaction create --content type=string FLAG fizzy reaction create --count type=bool FLAG fizzy reaction create --help type=bool FLAG fizzy reaction create --ids-only type=bool +FLAG fizzy reaction create --jq type=string FLAG fizzy reaction create --json type=bool FLAG fizzy reaction create --limit type=int FLAG fizzy reaction create --markdown type=bool @@ -1615,6 +1720,7 @@ FLAG fizzy reaction delete --comment type=string FLAG fizzy reaction delete --count type=bool FLAG fizzy reaction delete --help type=bool FLAG fizzy reaction delete --ids-only type=bool +FLAG fizzy reaction delete --jq type=string FLAG fizzy reaction delete --json type=bool FLAG fizzy reaction delete --limit type=int FLAG fizzy reaction delete --markdown type=bool @@ -1628,6 +1734,7 @@ FLAG fizzy reaction help --api-url type=string FLAG fizzy reaction help --count type=bool FLAG fizzy reaction help --help type=bool FLAG fizzy reaction help --ids-only type=bool +FLAG fizzy reaction help --jq type=string FLAG fizzy reaction help --json type=bool FLAG fizzy reaction help --limit type=int FLAG fizzy reaction help --markdown type=bool @@ -1643,6 +1750,7 @@ FLAG fizzy reaction list --comment type=string FLAG fizzy reaction list --count type=bool FLAG fizzy reaction list --help type=bool FLAG fizzy reaction list --ids-only type=bool +FLAG fizzy reaction list --jq type=string FLAG fizzy reaction list --json type=bool FLAG fizzy reaction list --limit type=int FLAG fizzy reaction list --markdown type=bool @@ -1660,6 +1768,7 @@ FLAG fizzy search --count type=bool FLAG fizzy search --help type=bool FLAG fizzy search --ids-only type=bool FLAG fizzy search --indexed-by type=string +FLAG fizzy search --jq type=string FLAG fizzy search --json type=bool FLAG fizzy search --limit type=int FLAG fizzy search --markdown type=bool @@ -1676,6 +1785,7 @@ FLAG fizzy setup --api-url type=string FLAG fizzy setup --count type=bool FLAG fizzy setup --help type=bool FLAG fizzy setup --ids-only type=bool +FLAG fizzy setup --jq type=string FLAG fizzy setup --json type=bool FLAG fizzy setup --limit type=int FLAG fizzy setup --markdown type=bool @@ -1689,6 +1799,7 @@ FLAG fizzy setup claude --api-url type=string FLAG fizzy setup claude --count type=bool FLAG fizzy setup claude --help type=bool FLAG fizzy setup claude --ids-only type=bool +FLAG fizzy setup claude --jq type=string FLAG fizzy setup claude --json type=bool FLAG fizzy setup claude --limit type=int FLAG fizzy setup claude --markdown type=bool @@ -1702,6 +1813,7 @@ FLAG fizzy setup help --api-url type=string FLAG fizzy setup help --count type=bool FLAG fizzy setup help --help type=bool FLAG fizzy setup help --ids-only type=bool +FLAG fizzy setup help --jq type=string FLAG fizzy setup help --json type=bool FLAG fizzy setup help --limit type=int FLAG fizzy setup help --markdown type=bool @@ -1715,6 +1827,7 @@ FLAG fizzy signup --api-url type=string FLAG fizzy signup --count type=bool FLAG fizzy signup --help type=bool FLAG fizzy signup --ids-only type=bool +FLAG fizzy signup --jq type=string FLAG fizzy signup --json type=bool FLAG fizzy signup --limit type=int FLAG fizzy signup --markdown type=bool @@ -1729,6 +1842,7 @@ FLAG fizzy signup complete --api-url type=string FLAG fizzy signup complete --count type=bool FLAG fizzy signup complete --help type=bool FLAG fizzy signup complete --ids-only type=bool +FLAG fizzy signup complete --jq type=string FLAG fizzy signup complete --json type=bool FLAG fizzy signup complete --limit type=int FLAG fizzy signup complete --markdown type=bool @@ -1743,6 +1857,7 @@ FLAG fizzy signup help --api-url type=string FLAG fizzy signup help --count type=bool FLAG fizzy signup help --help type=bool FLAG fizzy signup help --ids-only type=bool +FLAG fizzy signup help --jq type=string FLAG fizzy signup help --json type=bool FLAG fizzy signup help --limit type=int FLAG fizzy signup help --markdown type=bool @@ -1757,6 +1872,7 @@ FLAG fizzy signup start --count type=bool FLAG fizzy signup start --email type=string FLAG fizzy signup start --help type=bool FLAG fizzy signup start --ids-only type=bool +FLAG fizzy signup start --jq type=string FLAG fizzy signup start --json type=bool FLAG fizzy signup start --limit type=int FLAG fizzy signup start --markdown type=bool @@ -1771,6 +1887,7 @@ FLAG fizzy signup verify --code type=string FLAG fizzy signup verify --count type=bool FLAG fizzy signup verify --help type=bool FLAG fizzy signup verify --ids-only type=bool +FLAG fizzy signup verify --jq type=string FLAG fizzy signup verify --json type=bool FLAG fizzy signup verify --limit type=int FLAG fizzy signup verify --markdown type=bool @@ -1785,6 +1902,7 @@ FLAG fizzy skill --api-url type=string FLAG fizzy skill --count type=bool FLAG fizzy skill --help type=bool FLAG fizzy skill --ids-only type=bool +FLAG fizzy skill --jq type=string FLAG fizzy skill --json type=bool FLAG fizzy skill --limit type=int FLAG fizzy skill --markdown type=bool @@ -1798,6 +1916,7 @@ FLAG fizzy skill help --api-url type=string FLAG fizzy skill help --count type=bool FLAG fizzy skill help --help type=bool FLAG fizzy skill help --ids-only type=bool +FLAG fizzy skill help --jq type=string FLAG fizzy skill help --json type=bool FLAG fizzy skill help --limit type=int FLAG fizzy skill help --markdown type=bool @@ -1811,6 +1930,7 @@ FLAG fizzy skill install --api-url type=string FLAG fizzy skill install --count type=bool FLAG fizzy skill install --help type=bool FLAG fizzy skill install --ids-only type=bool +FLAG fizzy skill install --jq type=string FLAG fizzy skill install --json type=bool FLAG fizzy skill install --limit type=int FLAG fizzy skill install --markdown type=bool @@ -1824,6 +1944,7 @@ FLAG fizzy step --api-url type=string FLAG fizzy step --count type=bool FLAG fizzy step --help type=bool FLAG fizzy step --ids-only type=bool +FLAG fizzy step --jq type=string FLAG fizzy step --json type=bool FLAG fizzy step --limit type=int FLAG fizzy step --markdown type=bool @@ -1840,6 +1961,7 @@ FLAG fizzy step create --content type=string FLAG fizzy step create --count type=bool FLAG fizzy step create --help type=bool FLAG fizzy step create --ids-only type=bool +FLAG fizzy step create --jq type=string FLAG fizzy step create --json type=bool FLAG fizzy step create --limit type=int FLAG fizzy step create --markdown type=bool @@ -1854,6 +1976,7 @@ FLAG fizzy step delete --card type=string FLAG fizzy step delete --count type=bool FLAG fizzy step delete --help type=bool FLAG fizzy step delete --ids-only type=bool +FLAG fizzy step delete --jq type=string FLAG fizzy step delete --json type=bool FLAG fizzy step delete --limit type=int FLAG fizzy step delete --markdown type=bool @@ -1867,6 +1990,7 @@ FLAG fizzy step help --api-url type=string FLAG fizzy step help --count type=bool FLAG fizzy step help --help type=bool FLAG fizzy step help --ids-only type=bool +FLAG fizzy step help --jq type=string FLAG fizzy step help --json type=bool FLAG fizzy step help --limit type=int FLAG fizzy step help --markdown type=bool @@ -1881,6 +2005,7 @@ FLAG fizzy step list --card type=string FLAG fizzy step list --count type=bool FLAG fizzy step list --help type=bool FLAG fizzy step list --ids-only type=bool +FLAG fizzy step list --jq type=string FLAG fizzy step list --json type=bool FLAG fizzy step list --limit type=int FLAG fizzy step list --markdown type=bool @@ -1895,6 +2020,7 @@ FLAG fizzy step show --card type=string FLAG fizzy step show --count type=bool FLAG fizzy step show --help type=bool FLAG fizzy step show --ids-only type=bool +FLAG fizzy step show --jq type=string FLAG fizzy step show --json type=bool FLAG fizzy step show --limit type=int FLAG fizzy step show --markdown type=bool @@ -1911,6 +2037,7 @@ FLAG fizzy step update --content type=string FLAG fizzy step update --count type=bool FLAG fizzy step update --help type=bool FLAG fizzy step update --ids-only type=bool +FLAG fizzy step update --jq type=string FLAG fizzy step update --json type=bool FLAG fizzy step update --limit type=int FLAG fizzy step update --markdown type=bool @@ -1925,6 +2052,7 @@ FLAG fizzy tag --api-url type=string FLAG fizzy tag --count type=bool FLAG fizzy tag --help type=bool FLAG fizzy tag --ids-only type=bool +FLAG fizzy tag --jq type=string FLAG fizzy tag --json type=bool FLAG fizzy tag --limit type=int FLAG fizzy tag --markdown type=bool @@ -1938,6 +2066,7 @@ FLAG fizzy tag help --api-url type=string FLAG fizzy tag help --count type=bool FLAG fizzy tag help --help type=bool FLAG fizzy tag help --ids-only type=bool +FLAG fizzy tag help --jq type=string FLAG fizzy tag help --json type=bool FLAG fizzy tag help --limit type=int FLAG fizzy tag help --markdown type=bool @@ -1952,6 +2081,7 @@ FLAG fizzy tag list --api-url type=string FLAG fizzy tag list --count type=bool FLAG fizzy tag list --help type=bool FLAG fizzy tag list --ids-only type=bool +FLAG fizzy tag list --jq type=string FLAG fizzy tag list --json type=bool FLAG fizzy tag list --limit type=int FLAG fizzy tag list --markdown type=bool @@ -1966,6 +2096,7 @@ FLAG fizzy upload --api-url type=string FLAG fizzy upload --count type=bool FLAG fizzy upload --help type=bool FLAG fizzy upload --ids-only type=bool +FLAG fizzy upload --jq type=string FLAG fizzy upload --json type=bool FLAG fizzy upload --limit type=int FLAG fizzy upload --markdown type=bool @@ -1979,6 +2110,7 @@ FLAG fizzy upload file --api-url type=string FLAG fizzy upload file --count type=bool FLAG fizzy upload file --help type=bool FLAG fizzy upload file --ids-only type=bool +FLAG fizzy upload file --jq type=string FLAG fizzy upload file --json type=bool FLAG fizzy upload file --limit type=int FLAG fizzy upload file --markdown type=bool @@ -1992,6 +2124,7 @@ FLAG fizzy upload help --api-url type=string FLAG fizzy upload help --count type=bool FLAG fizzy upload help --help type=bool FLAG fizzy upload help --ids-only type=bool +FLAG fizzy upload help --jq type=string FLAG fizzy upload help --json type=bool FLAG fizzy upload help --limit type=int FLAG fizzy upload help --markdown type=bool @@ -2005,6 +2138,7 @@ FLAG fizzy user --api-url type=string FLAG fizzy user --count type=bool FLAG fizzy user --help type=bool FLAG fizzy user --ids-only type=bool +FLAG fizzy user --jq type=string FLAG fizzy user --json type=bool FLAG fizzy user --limit type=int FLAG fizzy user --markdown type=bool @@ -2018,6 +2152,7 @@ FLAG fizzy user avatar-remove --api-url type=string FLAG fizzy user avatar-remove --count type=bool FLAG fizzy user avatar-remove --help type=bool FLAG fizzy user avatar-remove --ids-only type=bool +FLAG fizzy user avatar-remove --jq type=string FLAG fizzy user avatar-remove --json type=bool FLAG fizzy user avatar-remove --limit type=int FLAG fizzy user avatar-remove --markdown type=bool @@ -2031,6 +2166,7 @@ FLAG fizzy user deactivate --api-url type=string FLAG fizzy user deactivate --count type=bool FLAG fizzy user deactivate --help type=bool FLAG fizzy user deactivate --ids-only type=bool +FLAG fizzy user deactivate --jq type=string FLAG fizzy user deactivate --json type=bool FLAG fizzy user deactivate --limit type=int FLAG fizzy user deactivate --markdown type=bool @@ -2044,6 +2180,7 @@ FLAG fizzy user help --api-url type=string FLAG fizzy user help --count type=bool FLAG fizzy user help --help type=bool FLAG fizzy user help --ids-only type=bool +FLAG fizzy user help --jq type=string FLAG fizzy user help --json type=bool FLAG fizzy user help --limit type=int FLAG fizzy user help --markdown type=bool @@ -2058,6 +2195,7 @@ FLAG fizzy user list --api-url type=string FLAG fizzy user list --count type=bool FLAG fizzy user list --help type=bool FLAG fizzy user list --ids-only type=bool +FLAG fizzy user list --jq type=string FLAG fizzy user list --json type=bool FLAG fizzy user list --limit type=int FLAG fizzy user list --markdown type=bool @@ -2074,6 +2212,7 @@ FLAG fizzy user push-subscription-create --count type=bool FLAG fizzy user push-subscription-create --endpoint type=string FLAG fizzy user push-subscription-create --help type=bool FLAG fizzy user push-subscription-create --ids-only type=bool +FLAG fizzy user push-subscription-create --jq type=string FLAG fizzy user push-subscription-create --json type=bool FLAG fizzy user push-subscription-create --limit type=int FLAG fizzy user push-subscription-create --markdown type=bool @@ -2089,6 +2228,7 @@ FLAG fizzy user push-subscription-delete --api-url type=string FLAG fizzy user push-subscription-delete --count type=bool FLAG fizzy user push-subscription-delete --help type=bool FLAG fizzy user push-subscription-delete --ids-only type=bool +FLAG fizzy user push-subscription-delete --jq type=string FLAG fizzy user push-subscription-delete --json type=bool FLAG fizzy user push-subscription-delete --limit type=int FLAG fizzy user push-subscription-delete --markdown type=bool @@ -2103,6 +2243,7 @@ FLAG fizzy user role --api-url type=string FLAG fizzy user role --count type=bool FLAG fizzy user role --help type=bool FLAG fizzy user role --ids-only type=bool +FLAG fizzy user role --jq type=string FLAG fizzy user role --json type=bool FLAG fizzy user role --limit type=int FLAG fizzy user role --markdown type=bool @@ -2117,6 +2258,7 @@ FLAG fizzy user show --api-url type=string FLAG fizzy user show --count type=bool FLAG fizzy user show --help type=bool FLAG fizzy user show --ids-only type=bool +FLAG fizzy user show --jq type=string FLAG fizzy user show --json type=bool FLAG fizzy user show --limit type=int FLAG fizzy user show --markdown type=bool @@ -2131,6 +2273,7 @@ FLAG fizzy user update --avatar type=string FLAG fizzy user update --count type=bool FLAG fizzy user update --help type=bool FLAG fizzy user update --ids-only type=bool +FLAG fizzy user update --jq type=string FLAG fizzy user update --json type=bool FLAG fizzy user update --limit type=int FLAG fizzy user update --markdown type=bool @@ -2145,6 +2288,7 @@ FLAG fizzy version --api-url type=string FLAG fizzy version --count type=bool FLAG fizzy version --help type=bool FLAG fizzy version --ids-only type=bool +FLAG fizzy version --jq type=string FLAG fizzy version --json type=bool FLAG fizzy version --limit type=int FLAG fizzy version --markdown type=bool @@ -2158,6 +2302,7 @@ FLAG fizzy webhook --api-url type=string FLAG fizzy webhook --count type=bool FLAG fizzy webhook --help type=bool FLAG fizzy webhook --ids-only type=bool +FLAG fizzy webhook --jq type=string FLAG fizzy webhook --json type=bool FLAG fizzy webhook --limit type=int FLAG fizzy webhook --markdown type=bool @@ -2173,6 +2318,7 @@ FLAG fizzy webhook create --board type=string FLAG fizzy webhook create --count type=bool FLAG fizzy webhook create --help type=bool FLAG fizzy webhook create --ids-only type=bool +FLAG fizzy webhook create --jq type=string FLAG fizzy webhook create --json type=bool FLAG fizzy webhook create --limit type=int FLAG fizzy webhook create --markdown type=bool @@ -2189,6 +2335,7 @@ FLAG fizzy webhook delete --board type=string FLAG fizzy webhook delete --count type=bool FLAG fizzy webhook delete --help type=bool FLAG fizzy webhook delete --ids-only type=bool +FLAG fizzy webhook delete --jq type=string FLAG fizzy webhook delete --json type=bool FLAG fizzy webhook delete --limit type=int FLAG fizzy webhook delete --markdown type=bool @@ -2202,6 +2349,7 @@ FLAG fizzy webhook help --api-url type=string FLAG fizzy webhook help --count type=bool FLAG fizzy webhook help --help type=bool FLAG fizzy webhook help --ids-only type=bool +FLAG fizzy webhook help --jq type=string FLAG fizzy webhook help --json type=bool FLAG fizzy webhook help --limit type=int FLAG fizzy webhook help --markdown type=bool @@ -2217,6 +2365,7 @@ FLAG fizzy webhook list --board type=string FLAG fizzy webhook list --count type=bool FLAG fizzy webhook list --help type=bool FLAG fizzy webhook list --ids-only type=bool +FLAG fizzy webhook list --jq type=string FLAG fizzy webhook list --json type=bool FLAG fizzy webhook list --limit type=int FLAG fizzy webhook list --markdown type=bool @@ -2232,6 +2381,7 @@ FLAG fizzy webhook reactivate --board type=string FLAG fizzy webhook reactivate --count type=bool FLAG fizzy webhook reactivate --help type=bool FLAG fizzy webhook reactivate --ids-only type=bool +FLAG fizzy webhook reactivate --jq type=string FLAG fizzy webhook reactivate --json type=bool FLAG fizzy webhook reactivate --limit type=int FLAG fizzy webhook reactivate --markdown type=bool @@ -2246,6 +2396,7 @@ FLAG fizzy webhook show --board type=string FLAG fizzy webhook show --count type=bool FLAG fizzy webhook show --help type=bool FLAG fizzy webhook show --ids-only type=bool +FLAG fizzy webhook show --jq type=string FLAG fizzy webhook show --json type=bool FLAG fizzy webhook show --limit type=int FLAG fizzy webhook show --markdown type=bool @@ -2261,6 +2412,7 @@ FLAG fizzy webhook update --board type=string FLAG fizzy webhook update --count type=bool FLAG fizzy webhook update --help type=bool FLAG fizzy webhook update --ids-only type=bool +FLAG fizzy webhook update --jq type=string FLAG fizzy webhook update --json type=bool FLAG fizzy webhook update --limit type=int FLAG fizzy webhook update --markdown type=bool diff --git a/go.mod b/go.mod index 3f9ea02..dbf9ebc 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/charmbracelet/huh v1.0.0 github.com/charmbracelet/lipgloss v1.1.0 github.com/charmbracelet/x/term v0.2.2 + github.com/itchyny/gojq v0.12.18 github.com/mattn/go-isatty v0.0.20 github.com/muesli/termenv v0.16.0 github.com/spf13/cobra v1.10.2 @@ -27,14 +28,17 @@ require ( github.com/charmbracelet/x/ansi v0.9.3 // indirect github.com/charmbracelet/x/cellbuf v0.0.13 // indirect github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect + github.com/clipperhouse/stringish v0.1.1 // indirect + github.com/clipperhouse/uax29/v2 v2.3.0 // indirect github.com/danieljoos/wincred v1.2.2 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/itchyny/timefmt-go v0.1.7 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-localereader v0.0.1 // indirect - github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/mattn/go-runewidth v0.0.19 // indirect github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect @@ -42,6 +46,6 @@ require ( github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/zalando/go-keyring v0.2.6 // indirect golang.org/x/sync v0.15.0 // indirect - golang.org/x/sys v0.36.0 // indirect + golang.org/x/sys v0.38.0 // indirect golang.org/x/text v0.23.0 // indirect ) diff --git a/go.sum b/go.sum index 37f70e5..389a9a5 100644 --- a/go.sum +++ b/go.sum @@ -42,6 +42,10 @@ github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8 github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo= github.com/charmbracelet/x/xpty v0.1.2 h1:Pqmu4TEJ8KeA9uSkISKMU3f+C1F6OGBn8ABuGlqCbtI= github.com/charmbracelet/x/xpty v0.1.2/go.mod h1:XK2Z0id5rtLWcpeNiMYBccNNBrP2IJnzHI0Lq13Xzq4= +github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs= +github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= +github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4= +github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= @@ -59,14 +63,18 @@ github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaU github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/itchyny/gojq v0.12.18 h1:gFGHyt/MLbG9n6dqnvlliiya2TaMMh6FFaR2b1H6Drc= +github.com/itchyny/gojq v0.12.18/go.mod h1:4hPoZ/3lN9fDL1D+aK7DY1f39XZpY9+1Xpjz8atrEkg= +github.com/itchyny/timefmt-go v0.1.7 h1:xyftit9Tbw+Dc/huSSPJaEmX1TVL8lw5vxjJLK4GMMA= +github.com/itchyny/timefmt-go v0.1.7/go.mod h1:5E46Q+zj7vbTgWY8o5YkMeYb4I6GeWLFnetPy5oBrAI= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= -github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= -github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= +github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= @@ -77,7 +85,6 @@ github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -103,8 +110,8 @@ golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= -golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= diff --git a/internal/commands/commands.go b/internal/commands/commands.go index 0c6ada3..e9ee20c 100644 --- a/internal/commands/commands.go +++ b/internal/commands/commands.go @@ -110,3 +110,20 @@ func agentHelp(cmd *cobra.Command, _ []string) { data, _ := json.MarshalIndent(info, "", " ") fmt.Fprintln(outWriter, string(data)) } + +// installAgentHelp sets the custom help function when --agent is active. +func installAgentHelp() { + rootCmd.SetHelpFunc(func(cmd *cobra.Command, args []string) { + if cfgAgent { + agentHelp(cmd, args) + return + } + // Banner on root help only + if cmd == rootCmd { + printBanner() + } + // Fall back to Cobra's default help + cmd.Root().SetHelpFunc(nil) + _ = cmd.Help() + }) +} diff --git a/internal/commands/completion.go b/internal/commands/completion.go index ba73d64..bc08668 100644 --- a/internal/commands/completion.go +++ b/internal/commands/completion.go @@ -5,6 +5,7 @@ import ( "os" "github.com/basecamp/cli/output" + "github.com/basecamp/fizzy-cli/internal/errors" "github.com/spf13/cobra" ) @@ -42,6 +43,9 @@ PowerShell: ValidArgs: []string{"bash", "zsh", "fish", "powershell"}, Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs), RunE: func(cmd *cobra.Command, args []string) error { + if cfgJQ != "" { + return errors.ErrJQNotSupported("completion script generation") + } var err error switch args[0] { case "bash": diff --git a/internal/commands/format_test.go b/internal/commands/format_test.go index 6093392..0f61b1c 100644 --- a/internal/commands/format_test.go +++ b/internal/commands/format_test.go @@ -254,6 +254,56 @@ func TestFormatJSONEnvelope(t *testing.T) { } } +func TestVersionJSONOutput(t *testing.T) { + mock := NewMockClient() + SetTestModeWithSDK(mock) + SetTestConfig("token", "account", "https://api.example.com") + defer resetTest() + + raw, err := runCobraWithArgs("version", "--json") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + var envelope map[string]any + if err := json.Unmarshal([]byte(raw), &envelope); err != nil { + t.Fatalf("expected JSON object, got parse error: %v\noutput: %s", err, raw) + } + if envelope["ok"] != true { + t.Errorf("expected ok=true, got %v", envelope["ok"]) + } + data, ok := envelope["data"].(map[string]any) + if !ok { + t.Fatalf("expected data object, got %T", envelope["data"]) + } + if data["version"] != rootCmd.Version { + t.Errorf("expected version %q, got %v", rootCmd.Version, data["version"]) + } +} + +func TestVersionQuietOutput(t *testing.T) { + mock := NewMockClient() + SetTestModeWithSDK(mock) + SetTestConfig("token", "account", "https://api.example.com") + defer resetTest() + + raw, err := runCobraWithArgs("version", "--quiet") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + var data map[string]any + if err := json.Unmarshal([]byte(raw), &data); err != nil { + t.Fatalf("expected JSON object, got parse error: %v\noutput: %s", err, raw) + } + if _, hasOK := data["ok"]; hasOK { + t.Error("quiet output should not include envelope") + } + if data["version"] != rootCmd.Version { + t.Errorf("expected version %q, got %v", rootCmd.Version, data["version"]) + } +} + func TestPrettyFlagRemoved(t *testing.T) { flag := rootCmd.PersistentFlags().Lookup("pretty") if flag != nil { @@ -281,6 +331,7 @@ func runCobraWithArgs(args ...string) (string, error) { cfgAgent = false cfgStyled = false cfgMarkdown = false + cfgJQ = "" testBuf.Reset() lastRawOutput = "" out = output.New(output.Options{Format: output.FormatJSON, Writer: &testBuf}) diff --git a/internal/commands/help_test.go b/internal/commands/help_test.go index d435515..9feaabb 100644 --- a/internal/commands/help_test.go +++ b/internal/commands/help_test.go @@ -15,7 +15,7 @@ func TestRenderRootHelp(t *testing.T) { renderHelp(rootCmd, &buf) out := buf.String() - for _, want := range []string{"CORE COMMANDS", "FLAGS", "--version", "GLOBAL OUTPUT FLAGS", "LEARN MORE"} { + for _, want := range []string{"CORE COMMANDS", "FLAGS", "--version", "GLOBAL OUTPUT FLAGS", "LEARN MORE", "implies --json"} { if !strings.Contains(out, want) { t.Fatalf("expected root help to contain %q, got:\n%s", want, out) } @@ -89,3 +89,15 @@ func TestRenderCommandsHelpMentionsJSONCatalog(t *testing.T) { t.Fatalf("expected commands help examples to include --json, got:\n%s", out) } } + +func TestRenderSubcommandHelpDoesNotRepeatJQFlag(t *testing.T) { + configureCLIUX() + + var buf bytes.Buffer + renderHelp(boardListCmd, &buf) + out := buf.String() + + if strings.Contains(out, "--jq") { + t.Fatalf("expected subcommand help to omit --jq, got:\n%s", out) + } +} diff --git a/internal/commands/jq.go b/internal/commands/jq.go new file mode 100644 index 0000000..ca5e3b3 --- /dev/null +++ b/internal/commands/jq.go @@ -0,0 +1,92 @@ +package commands + +import ( + "encoding/json" + "fmt" + "io" + "os" + + "github.com/basecamp/fizzy-cli/internal/errors" + "github.com/itchyny/gojq" +) + +// jqWriter wraps an io.Writer and applies a compiled jq filter to JSON output. +// Non-JSON writes pass through unchanged. +type jqWriter struct { + dest io.Writer + code *gojq.Code +} + +// newJQWriter parses and compiles the jq expression and returns a filtering writer. +// Delegates to compileJQ for compilation so options are maintained in one place. +func newJQWriter(dest io.Writer, filter string) (*jqWriter, error) { + code, err := compileJQ(filter) + if err != nil { + return nil, err + } + return &jqWriter{dest: dest, code: code}, nil +} + +// newJQWriterWithCode creates a jqWriter using a pre-compiled *gojq.Code. +// Used when the expression has already been validated and compiled (e.g. in PersistentPreRunE). +func newJQWriterWithCode(dest io.Writer, code *gojq.Code) *jqWriter { + return &jqWriter{dest: dest, code: code} +} + +// compileJQ parses and compiles a jq expression, returning the compiled code. +func compileJQ(filter string) (*gojq.Code, error) { + query, err := gojq.Parse(filter) + if err != nil { + return nil, errors.ErrJQValidation(err) + } + code, err := gojq.Compile(query, gojq.WithEnvironLoader(os.Environ)) + if err != nil { + return nil, errors.ErrJQValidation(err) + } + return code, nil +} + +// Write intercepts JSON output, applies the jq filter, and writes filtered results. +// String results print as plain text; everything else prints as compact single-line JSON. +// Error envelopes (ok: false) pass through unfiltered so error messages are never hidden. +func (w *jqWriter) Write(p []byte) (int, error) { + var input any + if err := json.Unmarshal(p, &input); err != nil { + // Not JSON — pass through unchanged. + return w.dest.Write(p) + } + + // Pass through error envelopes unfiltered so jq doesn't hide error messages. + if m, ok := input.(map[string]any); ok { + if okVal, exists := m["ok"]; exists { + if okBool, isBool := okVal.(bool); isBool && !okBool { + return w.dest.Write(p) + } + } + } + + iter := w.code.Run(input) + for { + v, ok := iter.Next() + if !ok { + break + } + if err, isErr := v.(error); isErr { + return 0, errors.ErrJQRuntime(err) + } + if s, isStr := v.(string); isStr { + if _, err := fmt.Fprintln(w.dest, s); err != nil { + return 0, err + } + } else { + raw, err := json.Marshal(v) + if err != nil { + return 0, errors.ErrJQRuntime(fmt.Errorf("result not serializable: %w", err)) + } + if _, err := fmt.Fprintln(w.dest, string(raw)); err != nil { + return 0, err + } + } + } + return len(p), nil +} diff --git a/internal/commands/jq_test.go b/internal/commands/jq_test.go new file mode 100644 index 0000000..cce54ec --- /dev/null +++ b/internal/commands/jq_test.go @@ -0,0 +1,712 @@ +package commands + +import ( + "encoding/json" + stderrors "errors" + "strings" + "testing" + + "github.com/basecamp/cli/output" + "github.com/basecamp/fizzy-cli/internal/client" + "github.com/basecamp/fizzy-cli/internal/errors" +) + +func TestJQFlagRegistered(t *testing.T) { + if rootCmd.PersistentFlags().Lookup("jq") == nil { + t.Error("expected --jq flag to be registered") + } +} + +func TestResolveFormatJQImpliesJSON(t *testing.T) { + defer resetTest() + + t.Run("--jq implies JSON", func(t *testing.T) { + resetTest() + cfgJQ = ".data" + f, err := resolveFormat() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if f != output.FormatJSON { + t.Errorf("expected FormatJSON, got %v", f) + } + }) + + t.Run("--jq --agent implies Quiet", func(t *testing.T) { + resetTest() + cfgJQ = ".data" + cfgAgent = true + f, err := resolveFormat() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if f != output.FormatQuiet { + t.Errorf("expected FormatQuiet, got %v", f) + } + }) + + t.Run("--jq --styled is an error", func(t *testing.T) { + resetTest() + cfgJQ = ".data" + cfgStyled = true + _, err := resolveFormat() + if err == nil { + t.Fatal("expected error for --jq --styled") + } + if !strings.Contains(err.Error(), "--jq filters JSON output") { + t.Errorf("unexpected error message: %v", err) + } + }) + + t.Run("--jq --markdown is an error", func(t *testing.T) { + resetTest() + cfgJQ = ".data" + cfgMarkdown = true + _, err := resolveFormat() + if err == nil { + t.Fatal("expected error for --jq --markdown") + } + }) + + t.Run("--jq --ids-only is an error", func(t *testing.T) { + resetTest() + cfgJQ = ".data" + cfgIDsOnly = true + _, err := resolveFormat() + if err == nil { + t.Fatal("expected error for --jq --ids-only") + } + }) + + t.Run("--jq --count is an error", func(t *testing.T) { + resetTest() + cfgJQ = ".data" + cfgCount = true + _, err := resolveFormat() + if err == nil { + t.Fatal("expected error for --jq --count") + } + }) + + t.Run("--jq --json is allowed", func(t *testing.T) { + resetTest() + cfgJQ = ".data" + cfgJSON = true + f, err := resolveFormat() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if f != output.FormatJSON { + t.Errorf("expected FormatJSON, got %v", f) + } + }) + + t.Run("--jq --quiet is allowed", func(t *testing.T) { + resetTest() + cfgJQ = ".data" + cfgQuiet = true + f, err := resolveFormat() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if f != output.FormatQuiet { + t.Errorf("expected FormatQuiet, got %v", f) + } + }) +} + +func TestJQIsMachineOutput(t *testing.T) { + defer resetTest() + resetTest() + cfgJQ = ".data" + if !IsMachineOutput() { + t.Error("expected IsMachineOutput to be true when --jq is set") + } +} + +func TestJQWriterExtractsField(t *testing.T) { + var buf strings.Builder + w, err := newJQWriter(&buf, ".data") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + input := `{"ok":true,"data":[{"id":"1","name":"Board 1"},{"id":"2","name":"Board 2"}]}` + if _, err := w.Write([]byte(input)); err != nil { + t.Fatalf("write error: %v", err) + } + + var result []map[string]any + if err := json.Unmarshal([]byte(buf.String()), &result); err != nil { + t.Fatalf("expected JSON array, got parse error: %v\noutput: %s", err, buf.String()) + } + if len(result) != 2 { + t.Errorf("expected 2 items, got %d", len(result)) + } +} + +func TestJQWriterStringOutput(t *testing.T) { + var buf strings.Builder + w, err := newJQWriter(&buf, ".data[0].name") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + input := `{"ok":true,"data":[{"id":"1","name":"Board 1"}]}` + if _, err := w.Write([]byte(input)); err != nil { + t.Fatalf("write error: %v", err) + } + + got := strings.TrimSpace(buf.String()) + if got != "Board 1" { + t.Errorf("expected plain string 'Board 1', got %q", got) + } +} + +func TestJQWriterSelect(t *testing.T) { + var buf strings.Builder + w, err := newJQWriter(&buf, `[.data[] | select(.completed == true)]`) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + input := `{"ok":true,"data":[{"id":"1","completed":true},{"id":"2","completed":false},{"id":"3","completed":true}]}` + if _, err := w.Write([]byte(input)); err != nil { + t.Fatalf("write error: %v", err) + } + + var result []map[string]any + if err := json.Unmarshal([]byte(buf.String()), &result); err != nil { + t.Fatalf("expected JSON array: %v\noutput: %s", err, buf.String()) + } + if len(result) != 2 { + t.Errorf("expected 2 completed items, got %d", len(result)) + } +} + +func TestJQWriterLength(t *testing.T) { + var buf strings.Builder + w, err := newJQWriter(&buf, ".data | length") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + input := `{"ok":true,"data":[{"id":"1"},{"id":"2"},{"id":"3"}]}` + if _, err := w.Write([]byte(input)); err != nil { + t.Fatalf("write error: %v", err) + } + + got := strings.TrimSpace(buf.String()) + if got != "3" { + t.Errorf("expected '3', got %q", got) + } +} + +func TestJQWriterInvalidExpression(t *testing.T) { + _, err := newJQWriter(nil, ".data[") + if err == nil { + t.Fatal("expected error for invalid jq expression") + } + if !strings.Contains(err.Error(), "invalid --jq expression") { + t.Errorf("expected 'invalid --jq expression' error, got: %v", err) + } +} + +func TestJQWriterMap(t *testing.T) { + var buf strings.Builder + w, err := newJQWriter(&buf, "[.data[] | {id, name}]") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + input := `{"ok":true,"data":[{"id":"1","name":"A","extra":"x"},{"id":"2","name":"B","extra":"y"}]}` + if _, err := w.Write([]byte(input)); err != nil { + t.Fatalf("write error: %v", err) + } + + var result []map[string]any + if err := json.Unmarshal([]byte(buf.String()), &result); err != nil { + t.Fatalf("expected JSON array: %v\noutput: %s", err, buf.String()) + } + if len(result) != 2 { + t.Errorf("expected 2 items, got %d", len(result)) + } + // Should not contain "extra" field + if _, hasExtra := result[0]["extra"]; hasExtra { + t.Error("expected 'extra' field to be excluded") + } +} + +func TestJQWriterPassthroughErrorEnvelope(t *testing.T) { + var buf strings.Builder + w, err := newJQWriter(&buf, ".data") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + // Error envelopes (ok: false) should pass through unfiltered + errorJSON := `{"ok":false,"error":"not found","code":"not_found"}` + if _, err := w.Write([]byte(errorJSON)); err != nil { + t.Fatalf("write error: %v", err) + } + + var result map[string]any + if err := json.Unmarshal([]byte(buf.String()), &result); err != nil { + t.Fatalf("expected JSON: %v\noutput: %s", err, buf.String()) + } + if result["ok"] != false { + t.Error("expected ok to be false") + } + if result["error"] != "not found" { + t.Errorf("expected error message preserved, got %v", result["error"]) + } +} + +func TestJQWriterPassthroughNonJSON(t *testing.T) { + var buf strings.Builder + w, err := newJQWriter(&buf, ".data") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + nonJSON := "not json at all" + if _, err := w.Write([]byte(nonJSON)); err != nil { + t.Fatalf("write error: %v", err) + } + if buf.String() != nonJSON { + t.Errorf("expected passthrough of non-JSON, got %q", buf.String()) + } +} + +func TestJQWriterIdentity(t *testing.T) { + var buf strings.Builder + w, err := newJQWriter(&buf, ".") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + input := `{"ok":true,"data":"hello"}` + if _, err := w.Write([]byte(input)); err != nil { + t.Fatalf("write error: %v", err) + } + + var result map[string]any + if err := json.Unmarshal([]byte(buf.String()), &result); err != nil { + t.Fatalf("expected JSON: %v\noutput: %s", err, buf.String()) + } + if result["ok"] != true { + t.Error("expected identity filter to pass through full object") + } +} + +func TestCobraJQExtractsData(t *testing.T) { + mock := NewMockClient() + mock.GetWithPaginationResponse = &client.APIResponse{ + StatusCode: 200, + Data: []map[string]any{ + {"id": "1", "name": "Board 1"}, + {"id": "2", "name": "Board 2"}, + }, + } + SetTestModeWithSDK(mock) + SetTestConfig("token", "account", "https://api.example.com") + defer resetTest() + + raw, err := runCobraWithArgs("board", "list", "--jq", ".data") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + var data []map[string]any + if err := json.Unmarshal([]byte(raw), &data); err != nil { + t.Fatalf("expected JSON array, got parse error: %v\noutput: %s", err, raw) + } + if len(data) != 2 { + t.Errorf("expected 2 items, got %d", len(data)) + } +} + +func TestCobraJQExtractsFieldAsString(t *testing.T) { + mock := NewMockClient() + mock.GetWithPaginationResponse = &client.APIResponse{ + StatusCode: 200, + Data: []map[string]any{ + {"id": "1", "name": "Board 1"}, + }, + } + SetTestModeWithSDK(mock) + SetTestConfig("token", "account", "https://api.example.com") + defer resetTest() + + raw, err := runCobraWithArgs("board", "list", "--jq", ".data[0].name") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + got := strings.TrimSpace(raw) + if got != "Board 1" { + t.Errorf("expected 'Board 1', got %q", got) + } +} + +func TestCobraJQInvalidExpression(t *testing.T) { + mock := NewMockClient() + SetTestModeWithSDK(mock) + SetTestConfig("token", "account", "https://api.example.com") + defer resetTest() + + _, err := runCobraWithArgs("board", "list", "--jq", ".data[") + if err == nil { + t.Fatal("expected error for invalid jq expression") + } +} + +func TestCobraJQRuntimeErrorIsReturned(t *testing.T) { + mock := NewMockClient() + mock.GetWithPaginationResponse = &client.APIResponse{ + StatusCode: 200, + Data: []map[string]any{ + {"id": "1", "name": "Board 1"}, + }, + } + SetTestModeWithSDK(mock) + SetTestConfig("token", "account", "https://api.example.com") + defer resetTest() + + _, err := runCobraWithArgs("board", "list", "--jq", ".data / 0") + if err == nil { + t.Fatal("expected error for jq runtime failure") + } + if !strings.Contains(err.Error(), "jq filter error") { + t.Fatalf("unexpected error: %v", err) + } + if !errors.IsJQError(err) { + t.Error("expected IsJQError to be true") + } +} + +// ============================================================================= +// Early validation tests (PersistentPreRunE) +// ============================================================================= + +func TestJQInvalidExpressionRejectedBeforeRunE(t *testing.T) { + mock := NewMockClient() + SetTestModeWithSDK(mock) + SetTestConfig("token", "account", "https://api.example.com") + defer resetTest() + + _, err := runCobraWithArgs("board", "list", "--jq", ".[invalid") + if err == nil { + t.Fatal("expected error for invalid jq expression") + } + if !strings.Contains(err.Error(), "invalid --jq expression") { + t.Errorf("expected 'invalid --jq expression', got: %v", err) + } + if !errors.IsJQError(err) { + t.Error("expected IsJQError to be true") + } +} + +func TestJQCompileErrorRejectedBeforeRunE(t *testing.T) { + mock := NewMockClient() + SetTestModeWithSDK(mock) + SetTestConfig("token", "account", "https://api.example.com") + defer resetTest() + + _, err := runCobraWithArgs("board", "list", "--jq", "$__loc__") + if err == nil { + t.Fatal("expected error for compile-time jq error") + } + if !strings.Contains(err.Error(), "invalid --jq expression") { + t.Errorf("expected 'invalid --jq expression', got: %v", err) + } +} + +func TestJQWithIDsOnlyConflict(t *testing.T) { + mock := NewMockClient() + SetTestModeWithSDK(mock) + SetTestConfig("token", "account", "https://api.example.com") + defer resetTest() + + _, err := runCobraWithArgs("board", "list", "--jq", ".data", "--ids-only") + if err == nil { + t.Fatal("expected error for --jq --ids-only conflict") + } + if !strings.Contains(err.Error(), "cannot use --jq with --ids-only") { + t.Errorf("unexpected error: %v", err) + } +} + +func TestJQWithCountConflict(t *testing.T) { + mock := NewMockClient() + SetTestModeWithSDK(mock) + SetTestConfig("token", "account", "https://api.example.com") + defer resetTest() + + _, err := runCobraWithArgs("board", "list", "--jq", ".data", "--count") + if err == nil { + t.Fatal("expected error for --jq --count conflict") + } + if !strings.Contains(err.Error(), "cannot use --jq with --count") { + t.Errorf("unexpected error: %v", err) + } +} + +// ============================================================================= +// env.VAR and $ENV.VAR access +// ============================================================================= + +func TestJQWriterEnvAccess(t *testing.T) { + t.Setenv("FIZZY_TEST_JQ_VAR", "hello_from_env") + + var buf strings.Builder + w, err := newJQWriter(&buf, `env.FIZZY_TEST_JQ_VAR`) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + input := `{"ok":true,"data":{"id":1}}` + if _, err := w.Write([]byte(input)); err != nil { + t.Fatalf("write error: %v", err) + } + if strings.TrimSpace(buf.String()) != "hello_from_env" { + t.Errorf("expected 'hello_from_env', got %q", buf.String()) + } +} + +func TestJQWriterEnvDollarAccess(t *testing.T) { + t.Setenv("FIZZY_TEST_JQ_VAR2", "dollar_form") + + var buf strings.Builder + w, err := newJQWriter(&buf, `$ENV.FIZZY_TEST_JQ_VAR2`) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + input := `{"ok":true,"data":{"id":1}}` + if _, err := w.Write([]byte(input)); err != nil { + t.Fatalf("write error: %v", err) + } + if strings.TrimSpace(buf.String()) != "dollar_form" { + t.Errorf("expected 'dollar_form', got %q", buf.String()) + } +} + +// ============================================================================= +// Compact output +// ============================================================================= + +func TestJQWriterCompactOutput(t *testing.T) { + var buf strings.Builder + w, err := newJQWriter(&buf, `.data`) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + input := `{"ok":true,"data":{"name":"test","value":42}}` + if _, err := w.Write([]byte(input)); err != nil { + t.Fatalf("write error: %v", err) + } + + out := strings.TrimSpace(buf.String()) + // Must be single-line compact JSON + if strings.Contains(out, "\n") { + t.Error("expected compact single-line JSON, got multi-line") + } + if !strings.Contains(out, `"name":"test"`) { + t.Errorf("expected compact JSON with name field, got %q", out) + } +} + +// ============================================================================= +// Edge cases: empty, null, multi-result, non-serializable +// ============================================================================= + +func TestJQWriterEmptyResult(t *testing.T) { + var buf strings.Builder + w, err := newJQWriter(&buf, `empty`) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + input := `{"ok":true,"data":{"id":1}}` + if _, err := w.Write([]byte(input)); err != nil { + t.Fatalf("write error: %v", err) + } + if buf.String() != "" { + t.Errorf("expected no output for empty result, got %q", buf.String()) + } +} + +func TestJQWriterNullResult(t *testing.T) { + var buf strings.Builder + w, err := newJQWriter(&buf, `null`) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + input := `{"ok":true,"data":{"id":1}}` + if _, err := w.Write([]byte(input)); err != nil { + t.Fatalf("write error: %v", err) + } + if strings.TrimSpace(buf.String()) != "null" { + t.Errorf("expected 'null', got %q", buf.String()) + } +} + +func TestJQWriterMultiResult(t *testing.T) { + var buf strings.Builder + w, err := newJQWriter(&buf, `.data.a, .data.b, .data.c`) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + input := `{"ok":true,"data":{"a":1,"b":"two","c":true}}` + if _, err := w.Write([]byte(input)); err != nil { + t.Fatalf("write error: %v", err) + } + + lines := strings.Split(strings.TrimSpace(buf.String()), "\n") + if len(lines) != 3 { + t.Fatalf("expected 3 lines, got %d: %v", len(lines), lines) + } + if lines[0] != "1" { + t.Errorf("expected '1', got %q", lines[0]) + } + if lines[1] != "two" { + t.Errorf("expected 'two', got %q", lines[1]) + } + if lines[2] != "true" { + t.Errorf("expected 'true', got %q", lines[2]) + } +} + +func TestJQWriterNonSerializableResultReturnsError(t *testing.T) { + for _, expr := range []string{"nan", "infinite"} { + t.Run(expr, func(t *testing.T) { + var buf strings.Builder + w, err := newJQWriter(&buf, expr) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + input := `{"ok":true,"data":{"id":1}}` + _, err = w.Write([]byte(input)) + if err == nil { + t.Fatal("expected error for non-serializable result") + } + if !strings.Contains(err.Error(), "not serializable") { + t.Errorf("expected 'not serializable' error, got: %v", err) + } + if !errors.IsJQError(err) { + t.Error("expected IsJQError to be true") + } + }) + } +} + +func TestJQWriterRuntimeError(t *testing.T) { + var buf strings.Builder + w, err := newJQWriter(&buf, `.data / 0`) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + input := `{"ok":true,"data":1}` + _, err = w.Write([]byte(input)) + if err == nil { + t.Fatal("expected runtime error") + } + if !errors.IsJQError(err) { + t.Error("expected IsJQError to be true for runtime error") + } + var outputErr *output.Error + if !stderrors.As(err, &outputErr) { + t.Error("expected error to be *output.Error") + } else if outputErr.Code != output.CodeUsage { + t.Errorf("expected CodeUsage, got %v", outputErr.Code) + } +} + +// ============================================================================= +// Command-level --jq rejection +// ============================================================================= + +func TestJQRejectedByCompletion(t *testing.T) { + mock := NewMockClient() + SetTestModeWithSDK(mock) + SetTestConfig("token", "account", "https://api.example.com") + defer resetTest() + + _, err := runCobraWithArgs("completion", "bash", "--jq", ".data") + if err == nil { + t.Fatal("expected error for --jq with completion") + } + if !strings.Contains(err.Error(), "--jq is not supported by") { + t.Errorf("unexpected error: %v", err) + } +} + +func TestJQRejectedBySkillInMachineMode(t *testing.T) { + defer resetTest() + resetTest() + cfgAgent = true + cfgJQ = ".data" + + err := runSkill(skillCmd, nil) + if err == nil { + t.Fatal("expected error for --jq with skill in machine mode") + } + if !strings.Contains(err.Error(), "--jq is not supported by") { + t.Errorf("unexpected error: %v", err) + } +} + +func TestJQRejectedByVersion(t *testing.T) { + mock := NewMockClient() + SetTestModeWithSDK(mock) + SetTestConfig("token", "account", "https://api.example.com") + defer resetTest() + + _, err := runCobraWithArgs("version", "--jq", ".version") + if err == nil { + t.Fatal("expected error for --jq with version") + } + if !strings.Contains(err.Error(), "--jq is not supported by the version command") { + t.Errorf("unexpected error: %v", err) + } +} + +func TestVersionOutputPlainText(t *testing.T) { + defer resetTest() + resetTest() + + var buf strings.Builder + outWriter = &buf + out = output.New(output.Options{Format: output.FormatStyled, Writer: &buf}) + + err := versionCmd.RunE(versionCmd, nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !strings.Contains(buf.String(), "fizzy version") { + t.Errorf("expected 'fizzy version' in output, got %q", buf.String()) + } +} + +func TestJQSetupBlockedInMachineMode(t *testing.T) { + defer resetTest() + resetTest() + cfgJQ = ".data" + + err := runSetup(setupCmd, nil) + if err == nil { + t.Fatal("expected error when running setup with --jq") + } + if !strings.Contains(err.Error(), "--jq is not supported by the setup command") { + t.Errorf("unexpected error: %v", err) + } + if !errors.IsJQError(err) { + t.Error("expected IsJQError to be true") + } +} diff --git a/internal/commands/root.go b/internal/commands/root.go index 5095c96..d9fb108 100644 --- a/internal/commands/root.go +++ b/internal/commands/root.go @@ -22,6 +22,7 @@ import ( "github.com/basecamp/fizzy-cli/internal/errors" "github.com/basecamp/fizzy-cli/internal/render" fizzy "github.com/basecamp/fizzy-sdk/go/pkg/fizzy" + "github.com/itchyny/gojq" "github.com/mattn/go-isatty" "github.com/spf13/cobra" ) @@ -43,6 +44,7 @@ var ( cfgStyled bool cfgMarkdown bool cfgLimit int + cfgJQ string // Loaded config cfg *config.Config @@ -72,6 +74,26 @@ var rootCmd = &cobra.Command{ Long: `Command-line interface for Fizzy`, Version: "dev", PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + errOutputWrite = nil + // Early jq validation: check flag conflicts first (actionable message), + // then parse + compile before RunE so invalid expressions are rejected + // with no side effects. The compiled code is reused below to avoid + // parsing the expression twice. + var jqCode *gojq.Code + if cfgJQ != "" { + if cfgIDsOnly { + return errors.ErrJQConflict("--ids-only") + } + if cfgCount { + return errors.ErrJQConflict("--count") + } + var err error + jqCode, err = compileJQ(cfgJQ) + if err != nil { + return err + } + } + // Resolve output format from parsed flags (must happen post-parse). format, err := resolveFormat() if err != nil { @@ -80,10 +102,18 @@ var rootCmd = &cobra.Command{ if lastResult != nil { // Test mode — preserve test buffer as writer. outWriter = &testBuf - out = output.New(output.Options{Format: format, Writer: &testBuf}) + var w io.Writer = &testBuf + if jqCode != nil { + w = newJQWriterWithCode(&testBuf, jqCode) + } + out = output.New(output.Options{Format: format, Writer: w}) } else { outWriter = os.Stdout - out = output.New(output.Options{Format: format, Writer: os.Stdout}) + var w io.Writer = os.Stdout + if jqCode != nil { + w = newJQWriterWithCode(os.Stdout, jqCode) + } + out = output.New(output.Options{Format: format, Writer: w}) } // In test mode, cfg is already set by SetTestConfig - don't overwrite @@ -140,6 +170,14 @@ var rootCmd = &cobra.Command{ return nil }, + PersistentPostRunE: func(cmd *cobra.Command, args []string) error { + if errOutputWrite != nil { + err := errOutputWrite + errOutputWrite = nil + return err + } + return nil + }, SilenceUsage: true, SilenceErrors: true, } @@ -170,6 +208,23 @@ func Execute() { // Cobra-level errors (arg count, unknown flag) → usage e = &output.Error{Code: output.CodeUsage, Message: err.Error()} } + + // jq-related errors (validation failures, unsupported commands, conflicts) + // must never be fed through the jq filter. Rebuild the output writer + // without jq so the error renders cleanly. Re-resolve the format to + // honor explicit flags like --agent --json. + if errors.IsJQError(err) && cfgJQ != "" { + format, fmtErr := resolveFormat() + if fmtErr != nil { + // resolveFormat() can fail when --jq conflicts with another flag + // (e.g. --jq --styled). Fall back to a sensible machine format. + format = output.FormatJSON + if cfgAgent || cfgQuiet { + format = output.FormatQuiet + } + } + out = output.New(output.Options{Format: format, Writer: outWriter}) + } if isHumanOutput() { printHumanError(cmd, e) } else { @@ -211,6 +266,11 @@ func resolveFormat() (output.Format, error) { return 0, fmt.Errorf("--agent and --styled cannot be used together") } + // --jq is a JSON transform and is incompatible with human/count/id renderers. + if cfgJQ != "" && (cfgStyled || cfgMarkdown || cfgIDsOnly || cfgCount) { + return 0, fmt.Errorf("--jq filters JSON output; use it with default JSON output or --quiet, not with --styled, --markdown, --ids-only, or --count") + } + // Explicit format flag wins switch { case cfgQuiet: @@ -227,6 +287,14 @@ func resolveFormat() (output.Format, error) { return output.FormatMarkdown, nil } + // --jq implies JSON (or quiet for --agent) + if cfgJQ != "" { + if cfgAgent { + return output.FormatQuiet, nil + } + return output.FormatJSON, nil + } + // --agent alone defaults to FormatQuiet if cfgAgent { return output.FormatQuiet, nil @@ -238,7 +306,7 @@ func resolveFormat() (output.Format, error) { // IsMachineOutput returns true when output should be treated as machine-consumable. // True when any machine format flag is set, --agent is set, or stdout/stdin is not a TTY. func IsMachineOutput() bool { - if cfgAgent || cfgJSON || cfgQuiet || cfgIDsOnly || cfgCount { + if cfgAgent || cfgJSON || cfgQuiet || cfgIDsOnly || cfgCount || cfgJQ != "" { return true } if !isatty.IsTerminal(os.Stdout.Fd()) && !isatty.IsCygwinTerminal(os.Stdout.Fd()) { @@ -312,6 +380,9 @@ func init() { rootCmd.PersistentFlags().BoolVar(&cfgStyled, "styled", false, "Styled terminal output with colors") rootCmd.PersistentFlags().BoolVar(&cfgMarkdown, "markdown", false, "Markdown formatted output") rootCmd.PersistentFlags().IntVar(&cfgLimit, "limit", 0, "Maximum number of results to display") + rootCmd.PersistentFlags().StringVar(&cfgJQ, "jq", "", "Apply jq filter to JSON output (built-in, no external jq required; implies --json)") + + installAgentHelp() } // getClient returns an API client configured from global settings. @@ -534,6 +605,20 @@ var testBuf bytes.Buffer // lastRawOutput holds the raw output from the last command (before buffer reset). var lastRawOutput string +// errOutputWrite stores the first output rendering/writer error from the current command. +var errOutputWrite error + +func recordOutputError(err error) { + if err != nil && errOutputWrite == nil { + errOutputWrite = err + } +} + +func writeOutputString(s string) { + _, err := io.WriteString(outWriter, s) + recordOutputError(err) +} + // captureResponse parses the writer buffer into lastResult after each shim call. func captureResponse() { if lastResult == nil { @@ -552,13 +637,13 @@ func captureResponse() { func printSuccess(data any) { switch out.EffectiveFormat() { case output.FormatStyled: - fmt.Fprint(outWriter, renderHumanData(data, "", false)) + writeOutputString(renderHumanData(data, "", false)) captureResponse() case output.FormatMarkdown: - fmt.Fprint(outWriter, renderHumanData(data, "", true)) + writeOutputString(renderHumanData(data, "", true)) captureResponse() default: - _ = out.OK(data) + recordOutputError(out.OK(data)) captureResponse() } } @@ -566,13 +651,13 @@ func printSuccess(data any) { func printSuccessWithLocation(location string) { switch out.EffectiveFormat() { case output.FormatStyled: - fmt.Fprint(outWriter, renderHumanData(nil, location, false)) + writeOutputString(renderHumanData(nil, location, false)) captureResponse() case output.FormatMarkdown: - fmt.Fprint(outWriter, renderHumanData(nil, location, true)) + writeOutputString(renderHumanData(nil, location, true)) captureResponse() default: - _ = out.OK(nil, output.WithContext("location", location)) + recordOutputError(out.OK(nil, output.WithContext("location", location))) captureResponse() } } @@ -588,16 +673,16 @@ func printSuccessWithBreadcrumbs(data any, summary string, breadcrumbs []Breadcr if summary != "" { opts = append(opts, output.WithSummary(summary)) } - _ = out.OK(data, opts...) + recordOutputError(out.OK(data, opts...)) captureResponse() } // printSuccessWithLocationAndBreadcrumbs prints a success response with both location and breadcrumbs. func printSuccessWithLocationAndBreadcrumbs(data any, location string, breadcrumbs []Breadcrumb) { - _ = out.OK(data, + recordOutputError(out.OK(data, output.WithBreadcrumbs(breadcrumbs...), output.WithContext("location", location), - ) + )) captureResponse() } @@ -661,11 +746,11 @@ func printList(data any, cols render.Columns, summary string, breadcrumbs []Brea switch out.EffectiveFormat() { case output.FormatStyled: body := render.StyledList(toMaps(data), cols, summary) - fmt.Fprint(outWriter, appendHumanSections(body, notice, "", breadcrumbs, false)) + writeOutputString(appendHumanSections(body, notice, "", breadcrumbs, false)) captureResponse() case output.FormatMarkdown: body := render.MarkdownList(toMaps(data), cols, summary) - fmt.Fprint(outWriter, appendHumanSections(body, notice, "", breadcrumbs, true)) + writeOutputString(appendHumanSections(body, notice, "", breadcrumbs, true)) captureResponse() default: opts := []output.ResponseOption{output.WithBreadcrumbs(breadcrumbs...)} @@ -675,7 +760,7 @@ func printList(data any, cols render.Columns, summary string, breadcrumbs []Brea if notice != "" { opts = append(opts, output.WithNotice(notice)) } - _ = out.OK(data, opts...) + recordOutputError(out.OK(data, opts...)) captureResponse() } } @@ -689,11 +774,11 @@ func printListPaginated(data any, cols render.Columns, hasNext bool, nextURL str switch out.EffectiveFormat() { case output.FormatStyled: body := render.StyledList(toMaps(data), cols, summary) - fmt.Fprint(outWriter, appendHumanSections(body, notice, "", breadcrumbs, false)) + writeOutputString(appendHumanSections(body, notice, "", breadcrumbs, false)) captureResponse() case output.FormatMarkdown: body := render.MarkdownList(toMaps(data), cols, summary) - fmt.Fprint(outWriter, appendHumanSections(body, notice, "", breadcrumbs, true)) + writeOutputString(appendHumanSections(body, notice, "", breadcrumbs, true)) captureResponse() default: opts := []output.ResponseOption{output.WithBreadcrumbs(breadcrumbs...)} @@ -709,7 +794,7 @@ func printListPaginated(data any, cols render.Columns, hasNext bool, nextURL str "next_url": nextURL, })) } - _ = out.OK(data, opts...) + recordOutputError(out.OK(data, opts...)) captureResponse() } } @@ -719,11 +804,11 @@ func printDetail(data any, summary string, breadcrumbs []Breadcrumb) { switch out.EffectiveFormat() { case output.FormatStyled: body := render.StyledDetail(toMap(data), summary) - fmt.Fprint(outWriter, appendHumanSections(body, "", "", breadcrumbs, false)) + writeOutputString(appendHumanSections(body, "", "", breadcrumbs, false)) captureResponse() case output.FormatMarkdown: body := render.MarkdownDetail(toMap(data), summary) - fmt.Fprint(outWriter, appendHumanSections(body, "", "", breadcrumbs, true)) + writeOutputString(appendHumanSections(body, "", "", breadcrumbs, true)) captureResponse() default: printSuccessWithBreadcrumbs(data, summary, breadcrumbs) @@ -735,11 +820,11 @@ func printMutationWithLocation(data any, location string, breadcrumbs []Breadcru switch out.EffectiveFormat() { case output.FormatStyled: body := render.StyledDetail(toMap(data), "") - fmt.Fprint(outWriter, appendHumanSections(body, "", location, breadcrumbs, false)) + writeOutputString(appendHumanSections(body, "", location, breadcrumbs, false)) captureResponse() case output.FormatMarkdown: body := render.MarkdownDetail(toMap(data), "") - fmt.Fprint(outWriter, appendHumanSections(body, "", location, breadcrumbs, true)) + writeOutputString(appendHumanSections(body, "", location, breadcrumbs, true)) captureResponse() default: printSuccessWithLocationAndBreadcrumbs(data, location, breadcrumbs) @@ -752,11 +837,11 @@ func printMutation(data any, summary string, breadcrumbs []Breadcrumb) { switch out.EffectiveFormat() { case output.FormatStyled: body := render.StyledSummary(toMap(data), summary) - fmt.Fprint(outWriter, appendHumanSections(body, "", "", breadcrumbs, false)) + writeOutputString(appendHumanSections(body, "", "", breadcrumbs, false)) captureResponse() case output.FormatMarkdown: body := render.MarkdownSummary(toMap(data), summary) - fmt.Fprint(outWriter, appendHumanSections(body, "", "", breadcrumbs, true)) + writeOutputString(appendHumanSections(body, "", "", breadcrumbs, true)) captureResponse() default: printSuccessWithBreadcrumbs(data, summary, breadcrumbs) @@ -1276,6 +1361,7 @@ func ResetTestMode() { errSDKInit = nil lastResult = nil lastRawOutput = "" + errOutputWrite = nil cfg = nil creds = nil profiles = nil @@ -1287,6 +1373,7 @@ func ResetTestMode() { cfgStyled = false cfgMarkdown = false cfgLimit = 0 + cfgJQ = "" cfgProfile = "" } diff --git a/internal/commands/setup.go b/internal/commands/setup.go index 9be352e..ff54cae 100644 --- a/internal/commands/setup.go +++ b/internal/commands/setup.go @@ -40,6 +40,9 @@ func init() { } func runSetup(cmd *cobra.Command, args []string) error { + if cfgJQ != "" { + return errors.ErrJQNotSupported("the setup command") + } if IsMachineOutput() { return output.ErrUsageHint("setup requires an interactive terminal", "Run without --agent/--json/--quiet or in a TTY") } diff --git a/internal/commands/skill.go b/internal/commands/skill.go index 9244f6d..65d3120 100644 --- a/internal/commands/skill.go +++ b/internal/commands/skill.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/basecamp/cli/output" + "github.com/basecamp/fizzy-cli/internal/errors" "github.com/basecamp/fizzy-cli/internal/harness" "github.com/basecamp/fizzy-cli/internal/skills" "github.com/charmbracelet/huh" @@ -46,6 +47,9 @@ func init() { func runSkill(cmd *cobra.Command, args []string) error { // Non-interactive: print skill content if IsMachineOutput() { + if cfgJQ != "" { + return errors.ErrJQNotSupported("the skill command") + } _, err := fmt.Fprint(cmd.OutOrStdout(), string(skills.Content)) return err } diff --git a/internal/commands/version.go b/internal/commands/version.go index bb842b3..ca3fea0 100644 --- a/internal/commands/version.go +++ b/internal/commands/version.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/basecamp/cli/output" + "github.com/basecamp/fizzy-cli/internal/errors" "github.com/spf13/cobra" ) @@ -12,9 +13,13 @@ var versionCmd = &cobra.Command{ Short: "Print version information", Example: "$ fizzy version", RunE: func(cmd *cobra.Command, args []string) error { + if cfgJQ != "" { + return errors.ErrJQNotSupported("the version command") + } switch out.EffectiveFormat() { case output.FormatStyled, output.FormatMarkdown: - fmt.Fprintf(outWriter, "fizzy version %s\n", rootCmd.Version) + _, err := fmt.Fprintf(outWriter, "fizzy version %s\n", rootCmd.Version) + recordOutputError(err) captureResponse() default: printSuccess(map[string]any{ diff --git a/internal/errors/errors.go b/internal/errors/errors.go index edf3b0c..6b5818b 100644 --- a/internal/errors/errors.go +++ b/internal/errors/errors.go @@ -3,6 +3,7 @@ package errors import ( + "errors" "fmt" "github.com/basecamp/cli/output" @@ -69,6 +70,55 @@ func NewNetworkError(message string) *CLIError { return e } +// errJQ is a sentinel cause for all jq-related errors (validation, +// unsupported command, flag conflict, and runtime failures). +// root.go uses IsJQError() to detect these and bypass jq filtering +// when rendering the error itself. +var errJQ = errors.New("jq error") + +// ErrJQValidation returns a usage error for invalid --jq expressions. +func ErrJQValidation(cause error) *CLIError { + return &output.Error{ + Code: output.CodeUsage, + Message: fmt.Sprintf("invalid --jq expression: %s", cause), + Cause: errJQ, + } +} + +// ErrJQNotSupported returns a usage error for commands that don't support --jq. +func ErrJQNotSupported(command string) *CLIError { + return &output.Error{ + Code: output.CodeUsage, + Message: fmt.Sprintf("--jq is not supported by %s", command), + Cause: errJQ, + } +} + +// ErrJQConflict returns a usage error for flags that conflict with --jq. +func ErrJQConflict(flag string) *CLIError { + return &output.Error{ + Code: output.CodeUsage, + Message: fmt.Sprintf("cannot use --jq with %s", flag), + Cause: errJQ, + } +} + +// ErrJQRuntime returns a usage error for jq runtime failures +// (e.g. type errors, non-serializable results). +func ErrJQRuntime(cause error) *CLIError { + return &output.Error{ + Code: output.CodeUsage, + Message: fmt.Sprintf("jq filter error: %s", cause), + Cause: errJQ, + } +} + +// IsJQError returns true if the error is a jq-related error +// (validation failure, unsupported command, flag conflict, or runtime failure). +func IsJQError(err error) bool { + return errors.Is(err, errJQ) +} + // FromHTTPStatus creates an appropriate error from an HTTP status code. func FromHTTPStatus(status int, message string) *CLIError { switch status { diff --git a/internal/errors/errors_test.go b/internal/errors/errors_test.go index 9293fe4..c6a2c7b 100644 --- a/internal/errors/errors_test.go +++ b/internal/errors/errors_test.go @@ -1,6 +1,8 @@ package errors import ( + stderrors "errors" + "fmt" "testing" "github.com/basecamp/cli/output" @@ -181,6 +183,99 @@ func TestNewInvalidArgsError(t *testing.T) { } } +// ============================================================================= +// JQ Error Constructor Tests +// ============================================================================= + +func TestErrJQValidation(t *testing.T) { + cause := fmt.Errorf("unexpected token") + err := ErrJQValidation(cause) + + if err.Code != output.CodeUsage { + t.Errorf("expected code %q, got %q", output.CodeUsage, err.Code) + } + if err.Message != "invalid --jq expression: unexpected token" { + t.Errorf("unexpected message: %q", err.Message) + } + if !IsJQError(err) { + t.Error("expected IsJQError to be true") + } +} + +func TestErrJQNotSupported(t *testing.T) { + err := ErrJQNotSupported("the version command") + + if err.Code != output.CodeUsage { + t.Errorf("expected code %q, got %q", output.CodeUsage, err.Code) + } + if err.Message != "--jq is not supported by the version command" { + t.Errorf("unexpected message: %q", err.Message) + } + if !IsJQError(err) { + t.Error("expected IsJQError to be true") + } +} + +func TestErrJQConflict(t *testing.T) { + err := ErrJQConflict("--ids-only") + + if err.Code != output.CodeUsage { + t.Errorf("expected code %q, got %q", output.CodeUsage, err.Code) + } + if err.Message != "cannot use --jq with --ids-only" { + t.Errorf("unexpected message: %q", err.Message) + } + if !IsJQError(err) { + t.Error("expected IsJQError to be true") + } +} + +func TestErrJQRuntime(t *testing.T) { + cause := fmt.Errorf("division by zero") + err := ErrJQRuntime(cause) + + if err.Code != output.CodeUsage { + t.Errorf("expected code %q, got %q", output.CodeUsage, err.Code) + } + if err.Message != "jq filter error: division by zero" { + t.Errorf("unexpected message: %q", err.Message) + } + if !IsJQError(err) { + t.Error("expected IsJQError to be true") + } +} + +func TestIsJQErrorFalseForNonJQError(t *testing.T) { + if IsJQError(NewInvalidArgsError("plain error")) { + t.Error("expected false for non-jq usage error") + } + if IsJQError(stderrors.New("random error")) { + t.Error("expected false for random error") + } + if IsJQError(NewNotFoundError("project not found")) { + t.Error("expected false for not-found error") + } +} + +func TestJQErrorExitCodes(t *testing.T) { + tests := []struct { + name string + err *CLIError + }{ + {"validation", ErrJQValidation(fmt.Errorf("test"))}, + {"not_supported", ErrJQNotSupported("test")}, + {"conflict", ErrJQConflict("--test")}, + {"runtime", ErrJQRuntime(fmt.Errorf("test"))}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.err.ExitCode() != ExitUsage { + t.Errorf("expected exit code %d, got %d", ExitUsage, tt.err.ExitCode()) + } + }) + } +} + func TestFromHTTPStatus(t *testing.T) { tests := []struct { name string diff --git a/internal/skills/SKILL.md b/internal/skills/SKILL.md index 6d4c22a..e3a6a6b 100644 --- a/internal/skills/SKILL.md +++ b/internal/skills/SKILL.md @@ -61,7 +61,7 @@ Full CLI coverage: boards, cards, columns, comments, steps, reactions, tags, use **MUST follow these rules:** 1. **Cards use NUMBER, not ID** — `fizzy card show 42` uses the card number. Other resources use their `id` field. -2. **Parse JSON with jq** to reduce token output — `fizzy card list | jq '[.data[] | {number, title}]'` +2. **Use built-in `--jq` for filtering** to reduce token output — `fizzy card list --jq '[.data[] | {number, title}]'`. Never pipe to external jq — use `--jq` instead. `--jq` implies `--json`, no need to pass both. 3. **Check breadcrumbs** in responses for available next actions with pre-filled values 4. **Check for board context** via `.fizzy.yaml` or `--board` flag before listing cards 5. **Rich text fields accept HTML** — use `
` tags for paragraphs, ` Feature description Feature description Looks good! Looks good! ` tags for paragraphs, ` Feature description Feature description Looks good! Looks good!