Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
# These are needed so that the linter does not complain
mkdir -p internal/legacy/archives
touch internal/legacy/archives/platform.phar
touch internal/legacy/archives/commands.json
touch internal/legacy/archives/commands-upsun.json internal/legacy/archives/commands-platformsh.json internal/legacy/archives/commands-vendor.json
touch internal/legacy/archives/php_windows_amd64
touch internal/legacy/archives/php_linux_amd64
touch internal/legacy/archives/php_linux_arm64
Expand Down Expand Up @@ -138,6 +138,9 @@ jobs:
- name: Build CLI
run: make single

- name: Install shells for completion tests
run: sudo apt-get update && sudo apt-get install -y bash-completion zsh fish

- name: Run integration tests
run: make integration-test

Expand All @@ -164,7 +167,7 @@ jobs:
curl -fSL https://curl.se/ca/cacert.pem -o internal/legacy/archives/cacert.pem
# Only needed so that the package builds.
touch internal/legacy/archives/platform.phar
touch internal/legacy/archives/commands.json
touch internal/legacy/archives/commands-upsun.json internal/legacy/archives/commands-platformsh.json

- name: Run Windows tests
shell: bash
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ The CLI operates as a wrapper around a legacy PHP CLI:
- Go layer: Handles new commands (init, list, version, config:install, project:convert) and core infrastructure
- PHP layer: Legacy commands are proxied through `internal/legacy/CLIWrapper`
- The PHP CLI (platform.phar) is embedded at build time via go:embed
- An index of legacy commands (commands.json, from `list --all --format=json`) is embedded too, so the Go layer can resolve abbreviations like `p:init` in the same way as Symfony Console
- An index of legacy commands (`commands-<flavor>.json`, from `list --all --format=json` with the embedded config) is embedded too, so the Go layer can resolve abbreviations like `p:init` in the same way as Symfony Console, and answer shell completion (`_complete`) for command and option names; PHP is only started for argument and option values, or when a user config file or environment variable may change the commands

### Key Components

Expand Down
37 changes: 23 additions & 14 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,25 @@ internal/legacy/archives/platform.phar: legacy/vendor/autoload.php
mkdir -p internal/legacy/archives
cd legacy && php bin/platform self:build --no-interaction --output=../internal/legacy/archives/platform.phar

# Index the legacy CLI's commands, so the Go layer can resolve abbreviations of its own commands.
# Experiments are enabled so that every command is included. HOME is isolated from user config.
# The version is set, as by the Go wrapper, so that the CLI does not try to read it from Git.
internal/legacy/archives/commands.json: internal/legacy/archives/platform.phar
# Index the legacy CLI's commands for each embedded config, so the Go layer can resolve abbreviations of its own
# commands and complete command and option names. HOME is isolated from user config. The version is set, as by the Go
# wrapper, so that the CLI does not try to read it from Git. Usage: $(call legacy-index,phar,config-file)
define legacy-index
tmp=$$(mktemp -d) && \
HOME=$$tmp PLATFORMSH_CLI_EXPERIMENTAL_ALL_EXPERIMENTS=1 PLATFORMSH_CLI_NO_LEGACY_WARNING=1 \
PLATFORMSH_CLI_APPLICATION_VERSION=0.0.0 \
php $< list --all --format=json --no-interaction < /dev/null > $$tmp/commands.json && \
prefix=$$(go run ./scripts/env-prefix $(2)) && \
env HOME=$$tmp CLI_CONFIG_FILE=$(abspath $(2)) "$${prefix}NO_LEGACY_WARNING=1" "$${prefix}APPLICATION_VERSION=0.0.0" \
php $(1) list --all --format=json --no-interaction < /dev/null > $$tmp/commands.json && \
mv $$tmp/commands.json $@; \
status=$$?; rm -rf $$tmp; exit $$status
endef

LEGACY_INDEXES = internal/legacy/archives/commands-upsun.json internal/legacy/archives/commands-platformsh.json

internal/legacy/archives/commands-%.json: internal/legacy/archives/platform.phar internal/config/%-cli.yaml
$(call legacy-index,$<,internal/config/$*-cli.yaml)

internal/legacy/archives/commands-vendor.json: internal/legacy/archives/platform.phar internal/config/embedded-config.yaml
$(call legacy-index,$<,internal/config/embedded-config.yaml)

legacy/vendor/autoload.php:
cd legacy && composer install --no-interaction
Expand Down Expand Up @@ -91,11 +100,11 @@ repogen:
command -v repogen >/dev/null || go install github.com/ralt/repogen/cmd/repogen@$(REPOGEN_VERSION)

.PHONY: single
single: goreleaser internal/legacy/archives/platform.phar internal/legacy/archives/commands.json php ## Build a single target release
single: goreleaser internal/legacy/archives/platform.phar $(LEGACY_INDEXES) php ## Build a single target release
PHP_VERSION=$(PHP_VERSION) goreleaser build --single-target --id=$(GORELEASER_ID) --snapshot --clean

.PHONY: snapshot
snapshot: goreleaser internal/legacy/archives/platform.phar internal/legacy/archives/commands.json php internal/legacy/archives/cacert.pem ## Build a snapshot release
snapshot: goreleaser internal/legacy/archives/platform.phar $(LEGACY_INDEXES) php internal/legacy/archives/cacert.pem ## Build a snapshot release
ifndef RSA_SIGNING_KEY_FILE
$(error RSA_SIGNING_KEY_FILE is not set. Set it to the path of your RSA private key for APK signing, or use 'make snapshot-no-nfpm' to skip packaging.)
endif
Expand All @@ -105,16 +114,16 @@ endif
PHP_VERSION=$(PHP_VERSION) goreleaser release --snapshot --clean --skip=publish,announce

.PHONY: snapshot-no-nfpm
snapshot-no-nfpm: goreleaser internal/legacy/archives/platform.phar internal/legacy/archives/commands.json php ## Build a snapshot release without package signing
snapshot-no-nfpm: goreleaser internal/legacy/archives/platform.phar $(LEGACY_INDEXES) php ## Build a snapshot release without package signing
PHP_VERSION=$(PHP_VERSION) goreleaser release --snapshot --clean --skip=publish,announce,nfpm

.PHONY: clean-phar
clean-phar: ## Clean up the legacy CLI phar
rm -f internal/legacy/archives/platform.phar internal/legacy/archives/commands.json
rm -f internal/legacy/archives/platform.phar internal/legacy/archives/commands-*.json
rm -rf legacy/vendor

.PHONY: release
release: goreleaser clean-phar internal/legacy/archives/platform.phar internal/legacy/archives/commands.json php internal/legacy/archives/cacert.pem ## Create and publish a release
release: goreleaser clean-phar internal/legacy/archives/platform.phar $(LEGACY_INDEXES) php internal/legacy/archives/cacert.pem ## Create and publish a release
ifndef RSA_SIGNING_KEY_FILE
$(error RSA_SIGNING_KEY_FILE is not set. Set it to the path of your RSA private key for APK signing.)
endif
Expand Down Expand Up @@ -155,11 +164,11 @@ ifndef VENDOR_BINARY
endif

.PHONY: vendor-release
vendor-release: check-vendor .goreleaser.vendor.yaml goreleaser clean-phar internal/legacy/archives/platform.phar internal/legacy/archives/commands.json php ## Release a vendor CLI
vendor-release: check-vendor .goreleaser.vendor.yaml goreleaser clean-phar internal/legacy/archives/platform.phar $(LEGACY_INDEXES) internal/legacy/archives/commands-vendor.json php ## Release a vendor CLI
PHP_VERSION=$(PHP_VERSION) VENDOR_BINARY="$(VENDOR_BINARY)" VENDOR_NAME="$(VENDOR_NAME)" goreleaser release --clean --config=.goreleaser.vendor.yaml

.PHONY: vendor-snapshot
vendor-snapshot: check-vendor .goreleaser.vendor.yaml goreleaser internal/legacy/archives/platform.phar internal/legacy/archives/commands.json php ## Build a vendor CLI snapshot
vendor-snapshot: check-vendor .goreleaser.vendor.yaml goreleaser internal/legacy/archives/platform.phar $(LEGACY_INDEXES) internal/legacy/archives/commands-vendor.json php ## Build a vendor CLI snapshot
PHP_VERSION=$(PHP_VERSION) VENDOR_BINARY="$(VENDOR_BINARY)" VENDOR_NAME="$(VENDOR_NAME)" goreleaser build --snapshot --clean --config=.goreleaser.vendor.yaml

.PHONY: goreleaser-check
Expand Down
17 changes: 9 additions & 8 deletions commands/abbreviation.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func expandAbbreviation(
}
candidates = append(candidates, abbrevCandidate{names: names, hidden: c.Hidden, native: true})
}
if nativeNames[name] || resolveAbbreviation(name, candidates) == nil {
if nativeNames[name] || resolveAbbreviation(name, candidates) == -1 {
return nil, false, nil
}

Expand All @@ -65,12 +65,12 @@ func expandAbbreviation(
candidates = append(candidates, abbrevCandidate{names: names, hidden: c.Hidden})
}

target := resolveAbbreviation(name, candidates)
if target == nil || !target.native {
i := resolveAbbreviation(name, candidates)
if i == -1 || !candidates[i].native {
return nil, false, nil
}
expanded = slices.Clone(args)
expanded[pos] = target.names[0]
expanded[pos] = candidates[i].names[0]
return expanded, true, nil
}

Expand Down Expand Up @@ -116,7 +116,8 @@ func enabledLegacyCommands(cnf *config.Config, load func() ([]legacy.Command, er
}

// resolveAbbreviation follows Symfony Console's rules to find the command abbreviated by name, if it is unique.
func resolveAbbreviation(name string, candidates []abbrevCandidate) *abbrevCandidate {
// It returns the index of the candidate, or -1.
func resolveAbbreviation(name string, candidates []abbrevCandidate) int {
parts := strings.Split(name, ":")
for i, p := range parts {
parts[i] = regexp.QuoteMeta(p)
Expand Down Expand Up @@ -147,13 +148,13 @@ func resolveAbbreviation(name string, candidates []abbrevCandidate) *abbrevCandi
// Prefix-only matches (e.g. "project" for "project:variable:get") count toward ambiguity, but at least one
// command must match fully.
if !fullMatch {
return nil
return -1
}

// Hidden commands still count toward ambiguity: the legacy CLI's lazy-loaded commands do not reliably report
// whether they are hidden, so it can resolve to them.
if len(matched) != 1 || candidates[matched[0]].hidden {
return nil
return -1
}
return &candidates[matched[0]]
return matched[0]
}
Loading
Loading