diff --git a/.surface-breaking b/.surface-breaking index 4ca0e78c5..579aa7a82 100644 --- a/.surface-breaking +++ b/.surface-breaking @@ -1513,3 +1513,61 @@ ARG basecamp folders move 00 ARG basecamp vault move 00 ARG basecamp vaults move 00 ARG basecamp tools move 00 +FLAG basecamp cards column copy --card-table type=string +FLAG basecamp cards copy --card-table type=string +FLAG basecamp checkin question copy --questionnaire type=string +FLAG basecamp checkin question move --questionnaire type=string +FLAG basecamp checkins question copy --questionnaire type=string +FLAG basecamp checkins question move --questionnaire type=string +FLAG basecamp docs copy --folder type=string +FLAG basecamp docs copy --vault type=string +FLAG basecamp docs move --folder type=string +FLAG basecamp docs move --vault type=string +FLAG basecamp documents copy --folder type=string +FLAG basecamp documents copy --vault type=string +FLAG basecamp documents move --folder type=string +FLAG basecamp documents move --vault type=string +FLAG basecamp file copy --folder type=string +FLAG basecamp file copy --vault type=string +FLAG basecamp file move --folder type=string +FLAG basecamp file move --vault type=string +FLAG basecamp files copy --folder type=string +FLAG basecamp files copy --vault type=string +FLAG basecamp files move --folder type=string +FLAG basecamp files move --vault type=string +FLAG basecamp folders copy --folder type=string +FLAG basecamp folders copy --vault type=string +FLAG basecamp folders move --folder type=string +FLAG basecamp folders move --vault type=string +FLAG basecamp forwards copy --inbox type=string +FLAG basecamp forwards move --inbox type=string +FLAG basecamp messages copy --message-board type=string +FLAG basecamp messages move --message-board type=string +FLAG basecamp msgs copy --message-board type=string +FLAG basecamp msgs move --message-board type=string +FLAG basecamp schedule copy --schedule type=string +FLAG basecamp schedule move --schedule type=string +FLAG basecamp tlgroup copy --list type=string +FLAG basecamp tlgroup move --list type=string +FLAG basecamp tlgroup position --list type=string +FLAG basecamp tlgroups copy --list type=string +FLAG basecamp tlgroups move --list type=string +FLAG basecamp tlgroups position --list type=string +FLAG basecamp todolistgroup copy --list type=string +FLAG basecamp todolistgroup move --list type=string +FLAG basecamp todolistgroup position --list type=string +FLAG basecamp todolistgroups copy --list type=string +FLAG basecamp todolistgroups move --list type=string +FLAG basecamp todolistgroups position --list type=string +FLAG basecamp uploads copy --folder type=string +FLAG basecamp uploads copy --vault type=string +FLAG basecamp uploads move --folder type=string +FLAG basecamp uploads move --vault type=string +FLAG basecamp vault copy --folder type=string +FLAG basecamp vault copy --vault type=string +FLAG basecamp vault move --folder type=string +FLAG basecamp vault move --vault type=string +FLAG basecamp vaults copy --folder type=string +FLAG basecamp vaults copy --vault type=string +FLAG basecamp vaults move --folder type=string +FLAG basecamp vaults move --vault type=string diff --git a/internal/cli/help.go b/internal/cli/help.go index 5890d78a2..05bae0159 100644 --- a/internal/cli/help.go +++ b/internal/cli/help.go @@ -411,6 +411,9 @@ func parentScopedFlags(cmd *cobra.Command) *pflag.FlagSet { if rootFlag != nil && rootFlag == f { return // root-level — stays in INHERITED FLAGS } + if commands.FilingIgnores(cmd, f) { + return + } ps.AddFlag(f) }) return ps diff --git a/internal/commands/filing.go b/internal/commands/filing.go index 533272971..67efb00e1 100644 --- a/internal/commands/filing.go +++ b/internal/commands/filing.go @@ -652,7 +652,7 @@ func guardFilingFlags(cmd *cobra.Command, reads ...string) *cobra.Command { cmd.Annotations[AnnotationFilingReads] = strings.Join(reads, " ") run := cmd.RunE cmd.RunE = func(cmd *cobra.Command, args []string) error { - if err := refuseUnreadContainerFlags(cmd, reads); err != nil { + if err := refuseUnreadContainerFlags(cmd); err != nil { return err } return run(cmd, args) @@ -660,10 +660,10 @@ func guardFilingFlags(cmd *cobra.Command, reads ...string) *cobra.Command { return cmd } -func refuseUnreadContainerFlags(cmd *cobra.Command, reads []string) error { +func refuseUnreadContainerFlags(cmd *cobra.Command) error { var unread *pflag.Flag cmd.InheritedFlags().VisitAll(func(f *pflag.Flag) { - if unread == nil && f.Changed && isContainerFlag(cmd, f) && !slices.Contains(reads, f.Name) { + if unread == nil && f.Changed && FilingIgnores(cmd, f) { unread = f } }) @@ -672,11 +672,19 @@ func refuseUnreadContainerFlags(cmd *cobra.Command, reads []string) error { } hint := fmt.Sprintf("%s takes no destination; drop --%s", cmd.CommandPath(), unread.Name) if dests := destinationFlags(cmd); len(dests) > 0 { - hint = fmt.Sprintf("Name where it goes with %s (see %s --help)", strings.Join(dests, " or "), cmd.CommandPath()) + hint = fmt.Sprintf("Drop --%s; to send it somewhere else, name where it goes with %s (see %s --help)", unread.Name, strings.Join(dests, " or "), cmd.CommandPath()) } return output.ErrUsageHint(fmt.Sprintf("%s does not read --%s, so it would ignore it", cmd.CommandPath(), unread.Name), hint) } +// FilingIgnores reports whether cmd moves or copies recordings and refuses the +// inherited flag f as a container flag it never reads. Help leaves such flags +// out, so it never offers one the command would refuse. +func FilingIgnores(cmd *cobra.Command, f *pflag.Flag) bool { + reads, guarded := cmd.Annotations[AnnotationFilingReads] + return guarded && isContainerFlag(cmd, f) && !slices.Contains(strings.Fields(reads), f.Name) +} + func isContainerFlag(cmd *cobra.Command, f *pflag.Flag) bool { switch f.Name { case "project", "in": diff --git a/internal/commands/filing_guard_test.go b/internal/commands/filing_guard_test.go index 73df77173..63d8be9d3 100644 --- a/internal/commands/filing_guard_test.go +++ b/internal/commands/filing_guard_test.go @@ -5,6 +5,7 @@ import ( "context" "io" "net/http" + "regexp" "slices" "sort" "strings" @@ -117,6 +118,32 @@ func TestFilingCommandsRefuseContainerFlagsTheyDoNotRead(t *testing.T) { assert.Positive(t, rows) } +// Help is how people and agents pick flags, so it must not offer one the +// command refuses: messages copy --help said --message-board was "required if +// project has multiple", and every copy that followed it failed. +func TestFilingCommandHelpOffersOnlyTheContainerFlagsItReads(t *testing.T) { + t.Setenv("BASECAMP_NO_KEYRING", "1") + hidden := 0 + for path, reads := range filingReads { + var buf bytes.Buffer + root := buildRootWithAllCommands() + root.SetOut(&buf) + root.SetArgs(append(strings.Fields(path)[1:], "--help")) + require.NoError(t, root.Execute()) + help := buf.String() + for _, flag := range inheritedContainerFlags(t, path) { + offered := regexp.MustCompile(`(?m)^\s+(-\w, )?--` + regexp.QuoteMeta(flag) + `\s`).MatchString(help) + if slices.Contains(reads, flag) { + assert.True(t, offered, "%s --help leaves out --%s, which it reads", path, flag) + continue + } + hidden++ + assert.False(t, offered, "%s --help offers --%s, which it refuses", path, flag) + } + } + assert.Positive(t, hidden) +} + func TestTodosCopyRefusesTheTodolistFlagItWouldIgnore(t *testing.T) { for name, args := range map[string][]string{ "after the command": {"todos", "copy", "456", "--todolist", "901"},