Skip to content

fix: resolve abbreviations of native commands - #176

Merged
pjcdawkins merged 5 commits into
mainfrom
claude/cli-52-e1422c
Sep 25, 2026
Merged

pjcdawkins merged 5 commits into
mainfrom
claude/cli-52-e1422c

Conversation

@pjcdawkins

@pjcdawkins pjcdawkins commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Abbreviations of Go-native commands, such as p:init or a:config-v, failed with errors like The namespace "p" is ambiguous: the Go layer only matched exact names, and the legacy CLI doesn't know about the native commands.

  • The build now generates internal/legacy/archives/commands.json from the phar (list --all --format=json, with all experiments enabled and an isolated HOME), so it lists every legacy command regardless of config. It is embedded, not committed.
  • The Go layer resolves abbreviations with Symfony Console's rules across native and legacy commands, and expands only a unique match to a native command. Anything else goes to the legacy CLI unchanged, including ambiguity errors.
  • The index is only parsed when a name uniquely abbreviates a native command, so legacy invocations cost nothing extra.
  • Expansion is only attempted after known boolean root flags, so a flag's value is never mistaken for the command name.
  • help <abbreviation> is resolved the same way.
  • Legacy commands disabled by the config (disabled_commands, wrapped_disabled_commands) are left out in Go. Hidden aliases only prevent expansion when typed in full, matching the legacy CLI.

🤖 Generated with Claude Code

@upsun-dispatch upsun-dispatch Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Warning

Changes suggested — 🟡 2 warnings · 🔵 2 minor points · ⚪ 1 nitpick

🔍 Full review · 10 files reviewed

⚪ Nitpick

  • commands/root.go:41 — The index-load failure is reported via debugLogf, which reads viper.GetBool("debug"). At this point ExecuteContext has not run, so the bound --debug flag is not parsed (Changed is false) and viper falls back to the env variable. A user passing --debug on the command line therefore never sees this message, so a corrupt or placeholder commands.json (e.g. the empty file CI touches) disables abbreviation resolution with no diagnosable output.
Verification
  • list --all --format=json emits commands as an object keyed by command name (CustomJsonDescriptor + DescriptorUtils), so map[string]Command unmarshals — I generated the real index and parsed it with the PR's struct.
  • Legacy entries whose canonical name matches a native name or alias are skipped, so list, help and completion do not create false ambiguity against their native overrides.
  • Replaying the resolver over the real 191-command index for every prefix-abbreviation of the native commands expands p:init, a:config-v, p:conv etc. and shadows no visible legacy command.
  • The Makefile recipe leaves $@ untouched and exits non-zero when the php run fails, and every goreleaser target that builds the phar now also lists commands.json, with clean-phar removing both.
  • Commands disabled by the upsun runtime config (project:variable:*, self:update, …) are present in the index but block no native abbreviation, so the config mismatch causes no missed expansion today.

New unit table test commands/abbreviation_test.go covers the resolver with a stubbed loader (run by the test CI job, which touches an empty commands.json, so real index parsing in internal/legacy/commands.go has no unit coverage), and integration-tests/abbreviation_test.go exercises the end-to-end path in the integration-test job, which builds the real index through make single. No test covers the failure path where the embedded index is missing or unparsable.

Review details
  • Commit: d37617d
  • Model: claude-opus-5

Review 1 of 10 for this pull request · View the full run

Comment thread Makefile Outdated
Comment thread commands/abbreviation.go Outdated
Comment thread commands/abbreviation.go
Comment thread internal/legacy/commands.go

@upsun-dispatch upsun-dispatch Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Note

Reviewed — No new blocking findings · 🔵 1 minor point · 3 still open (1 nitpick)

🔁 Incremental · 3 files reviewed

🔵 Minor point

  • commands/abbreviation.go:134 — The new comment and check make hidden commands count toward ambiguity, but hidden aliases never reach this code: the index is produced by CustomJsonDescriptor::getCommandData, which fills the aliases field from $command->getVisibleAliases() (legacy/src/Console/CustomJsonDescriptor.php:139), so aliases registered via setHiddenAliases() (e.g. logs, snapshots, user:role, i:act, local:install) are absent from commands.json. Symfony's Application::find() matches those aliases. An abbreviation that matches only a native command in the index but also matches a hidden alias is therefore expanded to the native command, whereas the legacy CLI would have reported ambiguity or run the legacy command. No current hidden alias collides with the small native command set, so this is latent today; adding a native command (or a vendor config with different aliases) makes it bite silently.

Outstanding from earlier reviews:

  • 🔵 #4098987925 — commands/abbreviation.go:32: Common legacy flags leave the reported bug unfixed for those invocations. — Only --help/-h were special-cased; -n (legacy's --no-interaction shorthand), -h inside a combined shorthand like -vh, and --help=true still fail isRootBoolFlag, so upsun -n p:init remains unexpanded.
  • 🔵 #4098987928 — internal/legacy/commands.go:12: Ships ~370KB of unused help text in every released binary. — The Makefile recipe still stores the full list --all --format=json output; commands.go still embeds it whole and reads only name/aliases/hidden.
  • ⚪ commands/root.go:41: Silent feature loss with no way to diagnose it from the command line. — commands/root.go still reports the index-load failure via debugLogf before ExecuteContext parses --debug. (first raised)
Verification
  • The --no-interaction plus < /dev/null added to the commands.json recipe suppresses SelfInstallChecker (it returns early when !isInteractive, legacy/src/Service/SelfInstallChecker.php:40) and the update check (Application.php:516).
  • PLATFORMSH_CLI_APPLICATION_VERSION maps to the existing application.version default key, so applyEnvironmentOverrides picks it up for the phar built from legacy/.
  • Removing the hidden-filter makes resolveAbbreviation strictly more conservative — a hidden competitor now yields nil and the args pass through to the legacy CLI unchanged.
  • slices is still used (IndexFunc/Contains/Clone) after deleting the DeleteFunc call, so abbreviation.go still compiles.
  • Treating --help/-h as a root boolean flag only affects the scan position; upsun --help p:init becomes --help init, which Cobra resolves to the native command's help.

Unit coverage is in commands/abbreviation_test.go (the ver case flipped to expect no expansion, plus a new --help p:init case), run by the test job, which embeds an empty touched commands.json; integration-tests/abbreviation_test.go exercises the real generated index in the integration-test job, which builds the phar and runs make single (thus the changed commands.json recipe) before make integration-test.

Review details

Review 2 of 10 for this pull request · View the full run

@pjcdawkins

Copy link
Copy Markdown
Contributor Author

Re: hidden aliases (review 5310891011): no change for now. Aliases registered with setHiddenAliases() in configure() are not resolved by the legacy CLI at all, since its commands are lazy-loaded and find() only sees names and AsCommand aliases. For example, snapshots, logs and user:role all fail with "Command ... is not defined" in the current release. That's a separate bug. Once it's fixed, the index should include hidden aliases too.

🤖 Addressed by Claude Code

pjcdawkins and others added 4 commits September 25, 2026 00:39
Abbreviations like "p:init" or "a:config-v" failed because the Go layer
only matched exact names, and the legacy CLI does not know about the
native commands.

The build now embeds the legacy CLI's "list --all --format=json" output,
generated from the phar with all experiments enabled, so it includes
every command regardless of config. The Go layer resolves abbreviations
with Symfony Console's rules across native and legacy commands, and
expands only a unique match to a native command. Everything else is
passed to the legacy CLI as before. The index is only parsed when a name
abbreviates a native command.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A value-taking flag before the command, e.g. "--context p:init init",
could have its value mistaken for the command name. Abbreviations are
now only expanded after known boolean root flags.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Set the version when generating the command index, as the Go wrapper
  does, so the phar does not run "git describe", which crashed in CI.
- Run the index generation non-interactively, so that local builds do
  not stop at the self-install prompt.
- Count hidden commands toward ambiguity: the legacy CLI can resolve to
  them, e.g. "ver" to the hidden "version:list".
- Allow --help and -h before an abbreviated command.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…y CLI

- Read the new hidden_aliases field of the command index. Hidden aliases
  only work in full, so they prevent expansion when typed exactly, but
  are not matched by abbreviations.
- Leave out legacy commands disabled by the config (disabled_commands
  and wrapped_disabled_commands), as the legacy CLI does not register
  them. The index itself stays config-independent, as one index is
  shared by all builds and the config can change at runtime.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@upsun-dispatch upsun-dispatch Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Warning

Changes suggested — 🟡 1 warning · 1 still open (1 nitpick)

🔍 Full review · 11 files reviewed

Outstanding from earlier reviews:

  • ⚪ commands/root.go:41: Silent feature loss with no way to diagnose it from the command line. — root.go still calls debugLogf before ExecuteContext has parsed flags, so passing --debug on the command line doesn't show the index-load failure. (first raised)
Verification
  • The legacy CustomJsonDescriptor emits commands keyed by name and a hidden_aliases field, which matches the Go map[string]Command and HiddenAliases JSON tags.
  • The Makefile recipe passes --no-interaction and < /dev/null and keeps the phar's exit status through status=$$?, so a failed list fails the build.
  • resolveAbbreviation requires one full match among matched names and counts prefix-only matches toward ambiguity, as Symfony's find() does with its {^expr$}i check.
  • An exact hidden legacy alias stops expansion, and a hidden alias never counts as an abbreviation candidate, matching HiddenAliasesCommandLoader.
  • release.yml builds through make release, which now depends on commands.json, so the go:embed input exists in release builds.

New table-driven unit tests in commands/abbreviation_test.go cover expandAbbreviation and enabledLegacyCommands (CI test job). integration-tests/abbreviation_test.go runs the real binary in the integration-test job. No test runs the --help &lt;abbrev> / -h &lt;abbrev> path end to end, and CI's test job embeds an empty commands.json.

Review details
  • Commit: 7811cf0
  • Model: claude-opus-5-5

Review 3 of 10 for this pull request · View the full run

Comment thread commands/abbreviation.go Outdated
Cobra adds the help flag during execution, after looking up the
command, so "--help init" treated "init" as the flag's value and passed
the arguments to the legacy CLI, which has no "init" command. This also
made expansion after "--help" ineffective.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@upsun-dispatch upsun-dispatch Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Note

Reviewed — No new issues found · 1 still open (1 nitpick)

🔁 Incremental · 4 files reviewed

Outstanding from earlier reviews:

  • ⚪ commands/root.go:41: Silent feature loss with no way to diagnose it from the command line. — Execute still calls debugLogf, which reads viper debug, before ExecuteContext parses flags, so the --debug command-line flag is ignored at that point. (first raised)
Verification
  • cmd.InitDefaultHelpFlag() is now called on root before Execute. --help is a bool with NoOptDefVal, so Cobra's stripFlags no longer takes the following init as its value.
  • isRootBoolFlag now checks root.Flags() as well as the persistent flags, so the local help/h flag counts as boolean without the old hard-coded --help/-h special case.
  • The unit test calls root.InitDefaultHelpFlag() in its setup, so the "after help flag" case runs through the same flag-lookup path as production.
  • No persistent root flag uses the h shorthand, so InitDefaultHelpFlag still registers -h on root.

This push adds unit cases in commands/abbreviation_test.go (help flag defined in the test setup) and two integration cases (--help p:init, --help init) in integration-tests/abbreviation_test.go. Both run under the repository's Go test jobs in .github/workflows/ci.yml.

Review details

Review 4 of 10 for this pull request · View the full run

@pjcdawkins

Copy link
Copy Markdown
Contributor Author

Re: the debugLogf nitpick on commands/root.go: leaving as is. The index can only fail to load with the placeholder file used for unit tests in CI. Real builds generate it with make, which fails if the list command fails. The message still shows with UPSUN_CLI_DEBUG=1, and parsing flags before Cobra runs isn't worth the extra code.

🤖 Addressed by Claude Code

@pjcdawkins
pjcdawkins merged commit 4f6186b into main Sep 25, 2026
6 checks passed
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.

1 participant