Skip to content

fix(list): list a namespace's commands the same way everywhere - #137

Open
vrobert78 wants to merge 1 commit into
mainfrom
fix/namespace-listing-consistency
Open

fix(list): list a namespace's commands the same way everywhere#137
vrobert78 wants to merge 1 commit into
mainfrom
fix/namespace-listing-consistency

Conversation

@vrobert78

Copy link
Copy Markdown

Problem

Naming a namespace lists different commands than list <namespace> does, and includes commands that are meant to be hidden:

$ upsun project                          $ upsun list project
project:clear-build-cache                project:clear-build-cache
project:create   [create]                project:create (create)
project:curl              <-- hidden
project:delete                           project:delete
project:get      [get]                   project:get (get)
project:info                             project:info
project:list     [projects|pro]          project:list (projects, pro)
project:set-remote                       project:set-remote (set-remote)
project:variable:delete   <-- hidden, deprecated
project:variable:get      <-- hidden, deprecated
project:variable:set      <-- hidden, deprecated

Asking for help on a namespace did not list it at all:

$ upsun help project
  Command "project" is ambiguous.
  Did you mean one of these?
      project:clear-build-cache  Clear a project's build cache
      project:create             Create a new project
      project:curl               Run an authenticated cURL request on a pr...   <-- hidden
      ...

$ upsun project --help
  ... the generic application help, with no namespace listing at all

And a command a vendor distribution disables through disabled_commands was still advertised. With the Akeneo configuration, which disables project:create:

$ CLI_CONFIG_FILE=vendor-config.yaml upsun project | grep project:create
  project:create             [create] Create a new project      <-- listed

$ CLI_CONFIG_FILE=vendor-config.yaml upsun project:create
  The command "project:create" is not enabled.                  <-- but cannot be run

Cause

Symfony reads isHidden() from the LazyCommand wrapper, which reports the AsCommand attribute it was built from. This CLI decides hidden-ness on the command itself: CommandBase::isHidden() combines the configured hidden_commands, the command's stability and its own flag, and deliberately ignores that attribute. The CLI's own list command is correct because DescriptorUtils::describeNamespaces() resolves the wrapper before asking.

Three places read the wrapper:

  • Application::doRun(), when the name is a namespace, describes it with a DescriptorHelper it builds itself — default descriptors, which also never check isEnabled(), hence the disabled commands.
  • Application::complete() skips $command->isHidden() when suggesting command names.
  • Application::getNamespaces() skips hidden commands when collecting namespaces.

Fix

Application::all() now resolves lazily-loaded commands, so the parent sees each command's own state wherever it enumerates them. Then Application::doRun() and the help command run the CLI's own list command for a namespace, so the formatting, the DEPRECATED markers and the disabled-command filtering match as well.

Streams and exit codes are unchanged: the bare namespace writes to the error output and reports that no command was run; help <namespace> writes to stdout and succeeds, since it was asked for. help <namespace> also honours --format, so it can produce the same JSON as list <namespace>.

Effect, measured

before after
upsun project / project --help / help project three different outputs, hidden commands listed all match list project
Tab-completion suggestions 282, incl. project:curl, api:curl, project:variable:* 248, no hidden ones
upsun proj Command "proj" is ambiguous lists the project namespace
disabled project:create, any listing path listed not listed
_complete latency, 5-run average 281 ms 331 ms

upsun proj improves because getNamespaces() was counting project:variable — a namespace that exists only because of the hidden deprecated commands — so any abbreviation matching both was ambiguous.

The ~50 ms is the cost of resolving every command, and it lands on anything that enumerates them, including each Tab press. Verified by driving real Tab presses in a Debian container: upsun project:<TAB> lists the seven visible commands and no longer offers curl or variable:*.

Verification

integration-tests/list_namespace_test.go covers the three listing paths, the abbreviation, the completion suggestions and a vendor configuration that disables a command. All three tests fail against unmodified main and pass here. Full integration suite green; golangci-lint, php-cs-fixer and PHPStan (level 8, PHP 8.4) clean; legacy PHPUnit at its existing baseline.

Known remaining difference

upsun list project includes project:init, which the namespace listings do not: that command is implemented in Go, and the Go list command adds it to the output the legacy CLI produces. The legacy CLI cannot know about it, so closing that gap would mean the Go layer intercepting namespace invocations, which needs the command list up front and would cost an extra PHP start on every pass-through. The test encodes this explicitly rather than hiding it.

🤖 Generated with Claude Code

Naming a namespace listed different commands than "list <namespace>", and
listed commands that are meant to be hidden:

    $ upsun project                       $ upsun list project
    project:clear-build-cache             project:clear-build-cache
    project:create   [create]             project:create (create)
    project:curl                <-- hidden
    project:delete                        project:delete
    project:get      [get]                project:get (get)
    project:info                          project:info
    project:list     [projects|pro]       project:list (projects, pro)
    project:set-remote                    project:set-remote (set-remote)
    project:variable:delete     <-- hidden, deprecated
    project:variable:get        <-- hidden, deprecated
    project:variable:set        <-- hidden, deprecated

Asking for help on a namespace did not list it at all: "upsun help project"
reported that the name was ambiguous, and the commands it suggested included
the hidden ones. "upsun project --help" printed the generic application help.

All of it comes from one cause: Symfony reads isHidden() from the LazyCommand
wrapper, which reports the AsCommand attribute it was built from, while this
CLI decides on the command itself — CommandBase::isHidden() combines the
configured hidden_commands, the command's stability and its own flag, and
deliberately ignores that attribute. The CLI's own list command is right
because DescriptorUtils::describeNamespaces() resolves the wrapper first.

Resolve lazily-loaded commands in Application::all(), so the parent sees each
command's own state wherever it enumerates them. That covers describing a
namespace, completing a command name, and collecting the namespaces
themselves: "upsun proj" used to be ambiguous only because the hidden
project:variable:* commands made "project:variable" a second namespace. It
costs about 50ms per invocation of anything that lists commands.

Then run our own list command for a namespace, from Application::doRun() and
from the help command, so the formatting matches too, along with the DEPRECATED
markers and the commands a vendor distribution disables through
disabled_commands — the default descriptors never checked isEnabled(), so those
were listed even though running them fails with "The command ... is not
enabled."

As before, the bare namespace writes to the error output and reports that no
command was run; help writes to stdout and succeeds, since it was asked for.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 29, 2026 20:32

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR makes namespace invocations (e.g., upsun project, upsun project --help, and upsun help project) list commands consistently with upsun list <namespace>, ensuring hidden and config-disabled commands are not advertised due to Symfony LazyCommand wrapper behavior.

Changes:

  • Resolve LazyCommand instances in the legacy Symfony Application::all() so enumeration uses each command’s real isHidden()/isEnabled() logic.
  • Route “namespace-as-command” execution (upsun <namespace>) through the CLI’s custom list command to unify formatting and filtering.
  • Enhance the legacy help command so help <namespace> lists the namespace via list, including --format/--raw support; add integration tests covering all listing paths, completion, and disabled commands.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.

File Description
legacy/src/Command/HelpCommand.php Detects when help is requested for a namespace and delegates output to list to match CLI formatting/filtering.
legacy/src/Application.php Resolves lazy-loaded commands during enumeration and uses list output for namespace invocations to avoid listing hidden/disabled commands.
integration-tests/list_namespace_test.go Adds integration coverage for consistent namespace listings, hidden-command suppression, completion suggestions, and vendor-disabled commands.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants