├── .gitattributes ├── .github ├── dependabot.yml └── workflows │ ├── base-docker.yml │ ├── go_test.yml │ ├── lint.yml │ ├── release.yml │ └── smoke-test.yml ├── .gitignore ├── .golangci.yml ├── .goreleaser.yaml ├── .vscode ├── launch.json └── settings.json ├── .windsurf └── rules │ ├── repository.md │ └── test.md ├── Dockerfile ├── Dockerfile.base ├── Dockerfile.bin ├── LICENSE ├── README.md ├── cmd ├── account │ ├── account.go │ ├── account_company.go │ ├── account_company_list.go │ ├── account_company_use.go │ ├── account_login.go │ ├── account_logout.go │ ├── account_merchant.go │ ├── account_merchant_shop.go │ ├── account_merchant_shop_composer.go │ ├── account_merchant_shop_list.go │ ├── account_producer.go │ ├── account_producer_extension.go │ ├── account_producer_extension_info.go │ ├── account_producer_extension_info_pull.go │ ├── account_producer_extension_info_push.go │ ├── account_producer_extension_list.go │ ├── account_producer_extension_upload.go │ └── account_producer_info.go ├── extension │ ├── extension.go │ ├── extension_admin_watch.go │ ├── extension_ai.go │ ├── extension_ai_twig_upgrade.go │ ├── extension_build.go │ ├── extension_changelog.go │ ├── extension_fix.go │ ├── extension_format.go │ ├── extension_prepare.go │ ├── extension_validate.go │ ├── extension_version.go │ ├── extension_zip.go │ └── static │ │ └── live-reload.js ├── project │ ├── ci.go │ ├── ci_test.go │ ├── config_sync.go │ ├── config_sync_entity.go │ ├── config_sync_mail_template.go │ ├── config_sync_system_config.go │ ├── config_sync_theme.go │ ├── platform.go │ ├── project.go │ ├── project_admin_api.go │ ├── project_admin_build.go │ ├── project_admin_watch.go │ ├── project_autofix.go │ ├── project_autofix_composer.go │ ├── project_autofix_flex.go │ ├── project_clear_cache.go │ ├── project_config.go │ ├── project_config_init.go │ ├── project_config_pull.go │ ├── project_config_push.go │ ├── project_console.go │ ├── project_create.go │ ├── project_debug.go │ ├── project_dump.go │ ├── project_extension.go │ ├── project_extension_activate.go │ ├── project_extension_deactivate.go │ ├── project_extension_delete.go │ ├── project_extension_install.go │ ├── project_extension_list.go │ ├── project_extension_outdated.go │ ├── project_extension_uninstall.go │ ├── project_extension_update.go │ ├── project_extension_upload.go │ ├── project_fix.go │ ├── project_format.go │ ├── project_generate_jwt.go │ ├── project_image_proxy.go │ ├── project_storefront_build.go │ ├── project_storefront_watch.go │ ├── project_upgrade_check.go │ ├── project_validate.go │ └── project_worker.go ├── root.go └── version.go ├── extension ├── _fixtures │ └── istorier.xml ├── app.go ├── app_test.go ├── asset.go ├── asset_platform.go ├── asset_platform_test.go ├── asset_test.go ├── build_modifier.go ├── build_modifier_test.go ├── bun_helper.go ├── bun_helper_test.go ├── bundle.go ├── bundle_test.go ├── caniuse_update.go ├── caniuse_update_test.go ├── changelog.go ├── changelog_test.go ├── composer-info.zip.zst ├── composer_info.go ├── config.go ├── config_test.go ├── git.go ├── manifest.go ├── manifest_test.go ├── platform.go ├── platform_test.go ├── project.go ├── project_test.go ├── root.go ├── shopware-extension-schema.json ├── snippet_validator.go ├── snippet_validator_test.go ├── theme.go ├── util.go ├── validator.go ├── validator_test.go ├── zip.go └── zip_test.go ├── go.mod ├── go.sum ├── internal ├── account-api │ ├── client.go │ ├── login.go │ ├── merchant.go │ ├── producer.go │ ├── producer_extension.go │ ├── profile.go │ └── updates.go ├── asset │ └── source.go ├── changelog │ ├── changelog.go │ ├── changelog.tpl │ └── changelog_test.go ├── color │ └── color.go ├── config │ ├── config.go │ ├── config_test.go │ └── testdata │ │ ├── .shopware-cli.yml │ │ └── write-test.yml ├── curl │ ├── curl_wrapper.go │ └── curl_wrapper_test.go ├── esbuild │ ├── download_unix.go │ ├── download_windows.go │ ├── esbuild.go │ ├── esbuild_test.go │ ├── fs.go │ ├── sass.go │ ├── sass_plugin.go │ ├── static │ │ ├── mixins.scss │ │ └── variables.scss │ ├── util.go │ ├── util_test.go │ ├── vite_config.go │ └── vite_config_test.go ├── flexmigrator │ ├── cleanup.go │ ├── cleanup_test.go │ ├── composer.go │ ├── composer_test.go │ ├── env.go │ └── env_test.go ├── git │ ├── git.go │ └── git_test.go ├── html │ ├── parser.go │ ├── parser_test.go │ └── testdata │ │ ├── 01-basic-element.txt │ │ ├── 02-sub-nodes.txt │ │ ├── 03-attributes-single.txt │ │ ├── 04-attributes.txt │ │ ├── 05-children-with-comment.txt │ │ ├── 06-multiple-comments.txt │ │ ├── 07-comment-with-nested-tags.txt │ │ ├── 08-comment-with-special-characters.txt │ │ ├── 09-elements-with-block.txt │ │ ├── 10-multi-line-breaks-get-removed.txt │ │ ├── 11-multi-line-between-elements-only-one.txt │ │ ├── 12-multi-line-between-only-elements.txt │ │ ├── 13-long-attribute-is-on-new-line.txt │ │ ├── 14-html-element-with-content.txt │ │ ├── 15-multiple-template-elements.txt │ │ ├── 16-multiple-template-elements-with-root.txt │ │ ├── 17-starting-tag-in-html-node.txt │ │ ├── 18-template-expression-in-div.txt │ │ ├── 19-multiple-template-expressions.txt │ │ ├── 20-template-expression-with-text.txt │ │ ├── 21-template-expression-in-nested-elements.txt │ │ ├── 22-template-expression-in-router-link.txt │ │ ├── 23-multiple-long-template-expressions.txt │ │ ├── 24-html-comment-before-element.txt │ │ ├── 25-if.txt │ │ ├── 26-if-else.txt │ │ ├── 27-if-elseif-else.txt │ │ ├── 28-if-while-attrs.txt │ │ ├── 29-block-nesting.txt │ │ ├── 30-attribute-long-closing-correctly-formatted.txt │ │ ├── 31-block-parent.txt │ │ ├── 32-multi-attribute-selfclose.txt │ │ ├── 33-comment-over-block.txt │ │ ├── 34-comment-over-block-nested.txt │ │ ├── 35-element-content-format.txt │ │ ├── 36-block-around-if.txt │ │ ├── 37-formatting-element.txt │ │ ├── 38-formatting-element-content-oneliner.txt │ │ ├── 39-attributes-escaped-content.txt │ │ └── 40-v-if-condition.txt ├── llm │ ├── gemini.go │ ├── main.go │ ├── openai.go │ ├── openai_test.go │ └── openrouter.go ├── packagist │ ├── auth.go │ ├── auth_test.go │ ├── composer.go │ ├── composer_test.go │ ├── lock.go │ ├── lock_test.go │ ├── packagist.go │ └── packagist_test.go ├── phpexec │ ├── phpexec.go │ └── phpexec_test.go ├── phplint │ ├── download.go │ ├── download_test.go │ ├── lint.go │ ├── lint_test.go │ ├── testdata │ │ ├── invalid.php │ │ └── valid.php │ └── wasm.go ├── spdx │ ├── license.go │ ├── license_test.go │ ├── spdx-exceptions.json │ └── spdx-licenses.json ├── system │ ├── cache.go │ ├── env.go │ ├── env_test.go │ ├── fs.go │ ├── fs_test.go │ ├── node.go │ ├── node_test.go │ ├── php.go │ └── php_test.go ├── table │ └── table.go ├── twigparser │ ├── node_list.go │ ├── nodes.go │ ├── parser_types.go │ ├── twig.go │ └── twig_test.go └── verifier │ ├── admin_twig.go │ ├── admintwiglinter │ ├── constants.go │ ├── fix_alert.go │ ├── fix_alert_test.go │ ├── fix_button.go │ ├── fix_button_test.go │ ├── fix_card.go │ ├── fix_card_test.go │ ├── fix_checkbox_field.go │ ├── fix_checkbox_field_test.go │ ├── fix_colorpicker.go │ ├── fix_colorpicker_test.go │ ├── fix_datepicker.go │ ├── fix_datepicker_test.go │ ├── fix_email_field.go │ ├── fix_email_field_test.go │ ├── fix_external_link.go │ ├── fix_external_link_test.go │ ├── fix_icon.go │ ├── fix_icon_test.go │ ├── fix_loader.go │ ├── fix_loader_test.go │ ├── fix_number_field.go │ ├── fix_number_field_test.go │ ├── fix_passwordfield.go │ ├── fix_passwordfield_test.go │ ├── fix_progress_bar.go │ ├── fix_progress_bar_test.go │ ├── fix_select_field.go │ ├── fix_select_field_test.go │ ├── fix_skeleton_bar.go │ ├── fix_skeleton_bar_test.go │ ├── fix_switch.go │ ├── fix_switch_test.go │ ├── fix_text_field.go │ ├── fix_text_field_test.go │ ├── fix_textareafield.go │ ├── fix_textareafield_test.go │ ├── fix_url_field.go │ ├── fix_url_field_test.go │ ├── fixer_popover.go │ ├── fixer_popover_test.go │ ├── helper_test.go │ └── root.go │ ├── composer.go │ ├── dir.go │ ├── embed.go │ ├── eslint.go │ ├── extension.go │ ├── js │ ├── .gitignore │ ├── configs │ │ ├── eslint.config.administration.mjs │ │ ├── eslint.config.storefront.mjs │ │ ├── prettierrc.js │ │ ├── stylelint.config.administration.mjs │ │ └── stylelint.config.storefront.mjs │ ├── package-lock.json │ ├── package.json │ └── packages │ │ └── @shopware-ag │ │ ├── admin-eslint-rules │ │ ├── 6.7 │ │ │ ├── require-explict-emits.js │ │ │ └── state-import.js │ │ ├── index.js │ │ ├── no-snippet-import.js │ │ ├── no-src-import.js │ │ └── package.json │ │ ├── admin-stylelint-rules │ │ ├── index.js │ │ ├── package.json │ │ └── wrong-scss-import.js │ │ └── storefront-eslint-rules │ │ ├── LICENSE │ │ ├── README.md │ │ ├── dom-access-helper.js │ │ ├── http-client.js │ │ ├── index.js │ │ ├── package.json │ │ ├── plugin-manager.js │ │ └── query-string.js │ ├── php │ ├── .gitignore │ ├── composer.json │ ├── composer.lock │ └── configs │ │ ├── php-cs-fixer.dist.php │ │ └── phpstan.neon │ ├── phpcsfixer.go │ ├── phpstan.go │ ├── phpstan_test.go │ ├── prettier.go │ ├── project.go │ ├── rector.go │ ├── reporter.go │ ├── result.go │ ├── result_test.go │ ├── stylelint.go │ ├── sw_cli.go │ └── tool.go ├── logging └── logger.go ├── main.go ├── scripts ├── completion.sh ├── entrypoint.sh └── schema.go └── shop ├── client.go ├── client_test.go ├── config.go ├── config_test.go ├── console.go ├── shopware-project-schema.json ├── version_check.go └── version_check_test.go /.gitattributes: -------------------------------------------------------------------------------- 1 | *.tpl eol=lf 2 | *.go eol=lf 3 | *.json eol=lf 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | commit-message: 8 | prefix: 'fix(deps): ' 9 | groups: 10 | all: 11 | patterns: 12 | - '*' 13 | - package-ecosystem: "github-actions" 14 | directory: "/" 15 | schedule: 16 | interval: "weekly" 17 | commit-message: 18 | prefix: 'fix(deps): ' 19 | groups: 20 | all: 21 | patterns: 22 | - '*' 23 | 24 | - package-ecosystem: "composer" 25 | directory: "/internal/verifier/php" 26 | schedule: 27 | interval: "weekly" 28 | commit-message: 29 | prefix: 'fix(deps): ' 30 | groups: 31 | all: 32 | patterns: 33 | - '*' 34 | 35 | - package-ecosystem: "npm" 36 | directory: "/internal/verifier/js" 37 | schedule: 38 | interval: "weekly" 39 | commit-message: 40 | prefix: 'fix(deps): ' 41 | groups: 42 | all: 43 | patterns: 44 | - '*' -------------------------------------------------------------------------------- /.github/workflows/base-docker.yml: -------------------------------------------------------------------------------- 1 | name: Update Base Docker Image 2 | on: 3 | workflow_dispatch: 4 | push: 5 | tags-ignore: 6 | - "*" 7 | branches: 8 | - main 9 | paths: 10 | - 'Dockerfile.base' 11 | - 'internal/verifier/js/**' 12 | - 'internal/verifier/php/**' 13 | 14 | env: 15 | DOCKER_BUILDKIT: 1 16 | 17 | jobs: 18 | build: 19 | name: Build PHP ${{ matrix.php-version }} 20 | runs-on: ubuntu-latest 21 | strategy: 22 | matrix: 23 | php-version: ["8.4", "8.3", "8.2", "8.1"] 24 | steps: 25 | - name: Harden Runner 26 | uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # ratchet:step-security/harden-runner@v2.12.0 27 | with: 28 | egress-policy: audit 29 | 30 | - name: Checkout 31 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # ratchet:actions/checkout@v4 32 | 33 | - name: Set up QEMU 34 | uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # ratchet:docker/setup-qemu-action@v3 35 | 36 | - name: Login into Github Docker Registry 37 | run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin 38 | 39 | - name: Set up Docker Buildx 40 | uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # ratchet:docker/setup-buildx-action@v3 41 | 42 | - name: Build and push 43 | uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # ratchet:docker/build-push-action@v6 44 | with: 45 | context: . 46 | push: true 47 | file: Dockerfile.base 48 | platforms: linux/amd64,linux/arm64 49 | tags: "ghcr.io/shopware/shopware-cli-base:${{ matrix.php-version }}" 50 | build-args: | 51 | PHP_VERSION=${{ matrix.php-version }} 52 | cache-from: type=gha 53 | cache-to: type=gha,mode=max 54 | provenance: false 55 | -------------------------------------------------------------------------------- /.github/workflows/go_test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | tags-ignore: 7 | - "*" 8 | pull_request: 9 | branches: ["main"] 10 | 11 | permissions: 12 | contents: read 13 | 14 | env: 15 | GOTOOLCHAIN: local 16 | 17 | jobs: 18 | build: 19 | name: ${{ matrix.os }} 20 | env: 21 | SHOPWARE_CLI_DISABLE_WASM_CACHE: 1 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | os: 26 | - ubuntu-latest 27 | - macos-14 28 | 29 | runs-on: ${{ matrix.os }} 30 | steps: 31 | - name: Checkout Repository 32 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # ratchet:actions/checkout@v4 33 | 34 | - name: Set up Go 35 | uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # ratchet:actions/setup-go@v5 36 | with: 37 | go-version: '1.24' 38 | check-latest: true 39 | cache: true 40 | 41 | - name: Build 42 | run: go build -v ./... 43 | 44 | - name: Test 45 | run: go test -v ./... 46 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: golangci-lint 2 | on: 3 | workflow_dispatch: 4 | push: 5 | tags-ignore: 6 | - "*" 7 | branches: 8 | - main 9 | paths: 10 | - '*.go' 11 | - '**/*.go' 12 | - '.github/workflows/lint.yml' 13 | pull_request: 14 | paths: 15 | - '*.go' 16 | - '**/*.go' 17 | - '.github/workflows/lint.yml' 18 | 19 | permissions: 20 | contents: read 21 | 22 | jobs: 23 | golangci: 24 | name: lint 25 | runs-on: ubuntu-latest 26 | steps: 27 | - name: Harden Runner 28 | uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # ratchet:step-security/harden-runner@v2.12.0 29 | with: 30 | egress-policy: block 31 | disable-sudo: true 32 | allowed-endpoints: > 33 | api.github.com:443 34 | github.com:443 35 | golangci-lint.run:443 36 | objects.githubusercontent.com:443 37 | proxy.golang.org:443 38 | raw.githubusercontent.com:443 39 | storage.googleapis.com:443 40 | 41 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # ratchet:actions/checkout@v4 42 | 43 | - name: Set up Go 44 | uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # ratchet:actions/setup-go@v5 45 | with: 46 | go-version: '1.24' 47 | check-latest: true 48 | cache: true 49 | 50 | - name: golangci-lint 51 | uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # ratchet:golangci/golangci-lint-action@v6 52 | with: 53 | version: latest 54 | args: --timeout 4m 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /.fleet 3 | /shopware-cli* 4 | dist/ 5 | /completions 6 | /local 7 | /.shopware-cli 8 | /.shopware-project.yml 9 | /.shopware-project.yaml 10 | /test-app 11 | /go-shopware-admin-api-sdk 12 | /*.zip 13 | /Frosh* 14 | /Swag* 15 | /result 16 | /project 17 | dump.sql* 18 | 19 | /testdata 20 | 21 | # Devenv 22 | .devenv* 23 | devenv.local.nix 24 | 25 | /.direnv 26 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | 3 | linters: 4 | enable: 5 | - bodyclose 6 | - exhaustive 7 | - goconst 8 | - goprintffuncname 9 | - govet 10 | - ineffassign 11 | - misspell 12 | - nakedret 13 | - noctx 14 | - staticcheck 15 | - unconvert 16 | - unparam 17 | - unused 18 | - whitespace 19 | - asciicheck 20 | - gocyclo 21 | - gocritic 22 | - errcheck 23 | - thelper 24 | - tparallel 25 | - predeclared 26 | - nilerr 27 | - makezero 28 | - forbidigo 29 | - errname 30 | - nilnil 31 | - usetesting 32 | - bidichk 33 | - containedctx 34 | - decorder 35 | - dogsled 36 | - dupword 37 | - durationcheck 38 | - errname 39 | - ginkgolinter 40 | - gocheckcompilerdirectives 41 | - goconst 42 | - godox 43 | - nilnil 44 | exclusions: 45 | rules: 46 | - path: cmd\/* 47 | linters: 48 | - forbidigo 49 | 50 | formatters: 51 | enable: 52 | - gofmt 53 | - gci 54 | settings: 55 | gci: 56 | sections: 57 | - Standard 58 | - Default 59 | - "Prefix(github.com/shopware/shopware-cli)" 60 | 61 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Attach Debugger", 6 | "type": "go", 7 | "request": "attach", 8 | "mode": "remote", 9 | "remotePath": "${workspaceFolder}", 10 | "port": 40001, 11 | "host": "localhost" 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "go.lintTool": "golangci-lint", 3 | "go.lintFlags": [ 4 | "--path-mode=abs", 5 | "--fast-only" 6 | ], 7 | "go.formatTool": "custom", 8 | "go.alternateTools": { 9 | "customFormatter": "golangci-lint" 10 | }, 11 | "go.formatFlags": [ 12 | "fmt", 13 | "--stdin" 14 | ], 15 | "go.useLanguageServer": true, 16 | "go.diagnostic.vulncheck": "Imports", 17 | "go.inlayHints.functionTypeParameters": true, 18 | "go.inlayHints.assignVariableTypes": true, 19 | "go.inlayHints.compositeLiteralFields": true, 20 | "go.inlayHints.constantValues": true, 21 | "files.exclude": { 22 | ".devenv": true, 23 | ".direnv": true, 24 | ".idea": true, 25 | }, 26 | "php.problems.exclude": { 27 | "internal/phplint/testdata/invalid.php": true 28 | }, 29 | "go.toolsManagement.autoUpdate": false, 30 | "go.toolsManagement.checkForUpdates": "off", 31 | "yaml.schemas": { 32 | "https://raw.githubusercontent.com/shopware/shopware-cli/main/extension/shopware-extension-schema.json": "file:///workspaces/shopware-cli/.shopware-extension.yml" 33 | }, 34 | } -------------------------------------------------------------------------------- /.windsurf/rules/repository.md: -------------------------------------------------------------------------------- 1 | --- 2 | trigger: always_on 3 | description: 4 | globs: 5 | --- 6 | # General rules 7 | 8 | - Use convential commit messages 9 | - Use always Go 1.24 syntax, use slices package for slice containing 10 | - Inline error into if condition like this: 11 | 12 | ```go 13 | if err := foo(); err != nil { 14 | // something 15 | } 16 | ``` 17 | 18 | -------------------------------------------------------------------------------- /.windsurf/rules/test.md: -------------------------------------------------------------------------------- 1 | --- 2 | trigger: glob 3 | globs: *_test.go 4 | --- 5 | 6 | - Use testify assert 7 | - Prefer assert.ElementsMatch on lists to ignore ordering issues 8 | - Use t.Setenv for environment variables 9 | - Use t.Context() for Context creation in tests -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG PHP_VERSION 2 | 3 | FROM ghcr.io/shopware/shopware-cli-base:${PHP_VERSION} 4 | 5 | COPY shopware-cli /usr/local/bin/ 6 | 7 | ENTRYPOINT ["/usr/local/bin/shopware-cli"] 8 | CMD ["--help"] 9 | -------------------------------------------------------------------------------- /Dockerfile.bin: -------------------------------------------------------------------------------- 1 | FROM scratch 2 | COPY shopware-cli / 3 | ENTRYPOINT ["/shopware-cli"] 4 | CMD ["--help"] 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Friends of Shopware 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Shopware CLI 2 | 3 | [![Hosted By: Cloudsmith](https://img.shields.io/badge/OSS%20hosting%20by-cloudsmith-blue?logo=cloudsmith&style=flat-square)](https://cloudsmith.com) 4 | 5 | A cli which contains handy helpful commands for daily Shopware tasks 6 | 7 | ## Features 8 | 9 | - Manage your Shopware account extensions in the CLI 10 | - Build and validate Shopware extensions 11 | 12 | For docs see [here](https://developer.shopware.com/docs/products/cli/) 13 | 14 | ## Contributing 15 | 16 | Contributions are always welcome! 17 | -------------------------------------------------------------------------------- /cmd/account/account.go: -------------------------------------------------------------------------------- 1 | package account 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | 6 | account_api "github.com/shopware/shopware-cli/internal/account-api" 7 | "github.com/shopware/shopware-cli/internal/config" 8 | ) 9 | 10 | var accountRootCmd = &cobra.Command{ 11 | Use: "account", 12 | Short: "Manage your Shopware Account", 13 | } 14 | 15 | type ServiceContainer struct { 16 | Conf config.Config 17 | AccountClient *account_api.Client 18 | } 19 | 20 | var services *ServiceContainer 21 | 22 | func Register(rootCmd *cobra.Command, onInit func(commandName string) (*ServiceContainer, error)) { 23 | accountRootCmd.PersistentPreRunE = func(cmd *cobra.Command, _ []string) error { 24 | ser, err := onInit(cmd.Name()) 25 | services = ser 26 | return err 27 | } 28 | rootCmd.AddCommand(accountRootCmd) 29 | } 30 | -------------------------------------------------------------------------------- /cmd/account/account_company.go: -------------------------------------------------------------------------------- 1 | package account 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | var accountCompanyRootCmd = &cobra.Command{ 8 | Use: "company", 9 | Short: "Manage your Shopware company", 10 | } 11 | 12 | func init() { 13 | accountRootCmd.AddCommand(accountCompanyRootCmd) 14 | } 15 | -------------------------------------------------------------------------------- /cmd/account/account_company_list.go: -------------------------------------------------------------------------------- 1 | package account 2 | 3 | import ( 4 | "os" 5 | "strconv" 6 | "strings" 7 | 8 | "github.com/spf13/cobra" 9 | 10 | "github.com/shopware/shopware-cli/internal/table" 11 | ) 12 | 13 | var accountCompanyListCmd = &cobra.Command{ 14 | Use: "list", 15 | Short: "Lists all available company for your Account", 16 | Aliases: []string{"ls"}, 17 | Long: ``, 18 | Run: func(_ *cobra.Command, _ []string) { 19 | table := table.NewWriter(os.Stdout) 20 | table.Header([]string{"ID", "Name", "Customer ID", "Roles"}) 21 | 22 | for _, membership := range services.AccountClient.GetMemberships() { 23 | _ = table.Append([]string{ 24 | strconv.FormatInt(int64(membership.Company.Id), 10), 25 | membership.Company.Name, 26 | membership.Company.CustomerNumber, 27 | strings.Join(membership.GetRoles(), ", "), 28 | }) 29 | } 30 | 31 | _ = table.Render() 32 | }, 33 | } 34 | 35 | func init() { 36 | accountCompanyRootCmd.AddCommand(accountCompanyListCmd) 37 | } 38 | -------------------------------------------------------------------------------- /cmd/account/account_company_use.go: -------------------------------------------------------------------------------- 1 | package account 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | 7 | "github.com/spf13/cobra" 8 | 9 | accountApi "github.com/shopware/shopware-cli/internal/account-api" 10 | "github.com/shopware/shopware-cli/logging" 11 | ) 12 | 13 | var accountCompanyUseCmd = &cobra.Command{ 14 | Use: "use [companyId]", 15 | Short: "Use another company for your Account", 16 | Args: cobra.MinimumNArgs(1), 17 | Long: ``, 18 | RunE: func(cmd *cobra.Command, args []string) error { 19 | companyID, err := strconv.Atoi(args[0]) 20 | if err != nil { 21 | return err 22 | } 23 | 24 | for _, membership := range services.AccountClient.GetMemberships() { 25 | if membership.Company.Id == companyID { 26 | if err := services.Conf.SetAccountCompanyId(companyID); err != nil { 27 | return err 28 | } 29 | 30 | if err := services.Conf.Save(); err != nil { 31 | return err 32 | } 33 | 34 | err = accountApi.InvalidateTokenCache() 35 | if err != nil { 36 | return fmt.Errorf("cannot invalidate token cache: %w", err) 37 | } 38 | 39 | logging.FromContext(cmd.Context()).Infof("Successfully changed your company to %s (%s)", membership.Company.Name, membership.Company.CustomerNumber) 40 | return nil 41 | } 42 | } 43 | 44 | return fmt.Errorf("company with ID \"%d\" not found", companyID) 45 | }, 46 | } 47 | 48 | func init() { 49 | accountCompanyRootCmd.AddCommand(accountCompanyUseCmd) 50 | } 51 | -------------------------------------------------------------------------------- /cmd/account/account_logout.go: -------------------------------------------------------------------------------- 1 | package account 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | 8 | accountApi "github.com/shopware/shopware-cli/internal/account-api" 9 | "github.com/shopware/shopware-cli/logging" 10 | ) 11 | 12 | var logoutCmd = &cobra.Command{ 13 | Use: "logout", 14 | Short: "Logout from Shopware Account", 15 | Long: ``, 16 | RunE: func(cmd *cobra.Command, _ []string) error { 17 | err := accountApi.InvalidateTokenCache() 18 | if err != nil { 19 | return fmt.Errorf("cannot invalidate token cache: %w", err) 20 | } 21 | 22 | _ = services.Conf.SetAccountCompanyId(0) 23 | _ = services.Conf.SetAccountEmail("") 24 | _ = services.Conf.SetAccountPassword("") 25 | 26 | if err := services.Conf.Save(); err != nil { 27 | return fmt.Errorf("cannot write config: %w", err) 28 | } 29 | 30 | logging.FromContext(cmd.Context()).Infof("You have been logged out") 31 | 32 | return nil 33 | }, 34 | } 35 | 36 | func init() { 37 | accountRootCmd.AddCommand(logoutCmd) 38 | } 39 | -------------------------------------------------------------------------------- /cmd/account/account_merchant.go: -------------------------------------------------------------------------------- 1 | package account 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | var accountCompanyMerchantCmd = &cobra.Command{ 8 | Use: "merchant", 9 | Short: "Manage merchants", 10 | } 11 | 12 | func init() { 13 | accountRootCmd.AddCommand(accountCompanyMerchantCmd) 14 | } 15 | -------------------------------------------------------------------------------- /cmd/account/account_merchant_shop.go: -------------------------------------------------------------------------------- 1 | package account 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | var accountCompanyMerchantShopCmd = &cobra.Command{ 8 | Use: "shop", 9 | Short: "Manage the shops", 10 | } 11 | 12 | func init() { 13 | accountCompanyMerchantCmd.AddCommand(accountCompanyMerchantShopCmd) 14 | } 15 | -------------------------------------------------------------------------------- /cmd/account/account_merchant_shop_list.go: -------------------------------------------------------------------------------- 1 | package account 2 | 3 | import ( 4 | "os" 5 | "strconv" 6 | 7 | "github.com/spf13/cobra" 8 | 9 | "github.com/shopware/shopware-cli/internal/table" 10 | ) 11 | 12 | var accountCompanyMerchantShopListCmd = &cobra.Command{ 13 | Use: "list", 14 | Short: "List all shops", 15 | Aliases: []string{"ls"}, 16 | RunE: func(cmd *cobra.Command, _ []string) error { 17 | table := table.NewWriter(os.Stdout) 18 | table.Header([]string{"ID", "Domain", "Usage"}) 19 | 20 | shops, err := services.AccountClient.Merchant().Shops(cmd.Context()) 21 | if err != nil { 22 | return err 23 | } 24 | 25 | for _, shop := range shops { 26 | _ = table.Append([]string{ 27 | strconv.FormatInt(int64(shop.Id), 10), 28 | shop.Domain, 29 | shop.Environment.Name, 30 | }) 31 | } 32 | 33 | _ = table.Render() 34 | 35 | return nil 36 | }, 37 | } 38 | 39 | func init() { 40 | accountCompanyMerchantShopCmd.AddCommand(accountCompanyMerchantShopListCmd) 41 | } 42 | -------------------------------------------------------------------------------- /cmd/account/account_producer.go: -------------------------------------------------------------------------------- 1 | package account 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | var accountCompanyProducerCmd = &cobra.Command{ 8 | Use: "producer", 9 | Short: "Manage your Shopware manufacturer", 10 | } 11 | 12 | func init() { 13 | accountRootCmd.AddCommand(accountCompanyProducerCmd) 14 | } 15 | -------------------------------------------------------------------------------- /cmd/account/account_producer_extension.go: -------------------------------------------------------------------------------- 1 | package account 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | var accountCompanyProducerExtensionCmd = &cobra.Command{ 8 | Use: "extension", 9 | Short: "Manage your Shopware extensions", 10 | } 11 | 12 | func init() { 13 | accountCompanyProducerCmd.AddCommand(accountCompanyProducerExtensionCmd) 14 | } 15 | -------------------------------------------------------------------------------- /cmd/account/account_producer_extension_info.go: -------------------------------------------------------------------------------- 1 | package account 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | var accountCompanyProducerExtensionInfoCmd = &cobra.Command{ 8 | Use: "info", 9 | Short: "Manage store page", 10 | } 11 | 12 | func init() { 13 | accountCompanyProducerExtensionCmd.AddCommand(accountCompanyProducerExtensionInfoCmd) 14 | } 15 | -------------------------------------------------------------------------------- /cmd/account/account_producer_extension_list.go: -------------------------------------------------------------------------------- 1 | package account 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strconv" 7 | 8 | "github.com/spf13/cobra" 9 | 10 | account_api "github.com/shopware/shopware-cli/internal/account-api" 11 | "github.com/shopware/shopware-cli/internal/table" 12 | ) 13 | 14 | var accountCompanyProducerExtensionListCmd = &cobra.Command{ 15 | Use: "list", 16 | Short: "Lists all your extensions", 17 | RunE: func(cmd *cobra.Command, _ []string) error { 18 | p, err := services.AccountClient.Producer(cmd.Context()) 19 | if err != nil { 20 | return fmt.Errorf("cannot get producer endpoint: %w", err) 21 | } 22 | 23 | criteria := account_api.ListExtensionCriteria{ 24 | Limit: 100, 25 | } 26 | 27 | if len(listExtensionSearch) > 0 { 28 | criteria.Search = listExtensionSearch 29 | criteria.OrderBy = "name" 30 | criteria.OrderSequence = "asc" 31 | } 32 | 33 | extensions, err := p.Extensions(cmd.Context(), &criteria) 34 | if err != nil { 35 | return err 36 | } 37 | 38 | table := table.NewWriter(os.Stdout) 39 | table.Header([]string{"ID", "Name", "Type", "Compatible with latest version", "Status"}) 40 | 41 | for _, extension := range extensions { 42 | if extension.Status.Name == "deleted" { 43 | continue 44 | } 45 | 46 | compatible := "No" 47 | 48 | if extension.IsCompatibleWithLatestShopwareVersion { 49 | compatible = "Yes" 50 | } 51 | 52 | _ = table.Append([]string{ 53 | strconv.FormatInt(int64(extension.Id), 10), 54 | extension.Name, 55 | extension.Generation.Description, 56 | compatible, 57 | extension.Status.Name, 58 | }) 59 | } 60 | 61 | _ = table.Render() 62 | 63 | return nil 64 | }, 65 | } 66 | 67 | var listExtensionSearch string 68 | 69 | func init() { 70 | accountCompanyProducerExtensionCmd.AddCommand(accountCompanyProducerExtensionListCmd) 71 | accountCompanyProducerExtensionListCmd.Flags().StringVar(&listExtensionSearch, "search", "", "Filter for name") 72 | } 73 | -------------------------------------------------------------------------------- /cmd/account/account_producer_info.go: -------------------------------------------------------------------------------- 1 | package account 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | 8 | "github.com/shopware/shopware-cli/logging" 9 | ) 10 | 11 | var accountProducerInfoCmd = &cobra.Command{ 12 | Use: "info", 13 | Short: "List information about your producer account", 14 | Long: ``, 15 | RunE: func(cmd *cobra.Command, _ []string) error { 16 | p, err := services.AccountClient.Producer(cmd.Context()) 17 | if err != nil { 18 | return fmt.Errorf("cannot get producer endpoint: %w", err) 19 | } 20 | 21 | profile, err := p.Profile(cmd.Context()) 22 | if err != nil { 23 | return fmt.Errorf("cannot get producer profile: %w", err) 24 | } 25 | 26 | logging.FromContext(cmd.Context()).Infof("Name: %s", profile.Name) 27 | logging.FromContext(cmd.Context()).Infof("Prefix: %s", profile.Prefix) 28 | logging.FromContext(cmd.Context()).Infof("Website: %s", profile.Website) 29 | 30 | return nil 31 | }, 32 | } 33 | 34 | func init() { 35 | accountCompanyProducerCmd.AddCommand(accountProducerInfoCmd) 36 | } 37 | -------------------------------------------------------------------------------- /cmd/extension/extension.go: -------------------------------------------------------------------------------- 1 | package extension 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | var extensionRootCmd = &cobra.Command{ 8 | Use: "extension", 9 | Short: "Shopware Extension utilities", 10 | } 11 | 12 | func Register(rootCmd *cobra.Command) { 13 | rootCmd.AddCommand(extensionRootCmd) 14 | } 15 | -------------------------------------------------------------------------------- /cmd/extension/extension_ai.go: -------------------------------------------------------------------------------- 1 | package extension 2 | 3 | import "github.com/spf13/cobra" 4 | 5 | var extensionAiCmd = &cobra.Command{ 6 | Use: "ai", 7 | Short: "AI commands (experimental)", 8 | } 9 | 10 | func init() { 11 | extensionRootCmd.AddCommand(extensionAiCmd) 12 | } 13 | -------------------------------------------------------------------------------- /cmd/extension/extension_build.go: -------------------------------------------------------------------------------- 1 | package extension 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/spf13/cobra" 9 | 10 | "github.com/shopware/shopware-cli/extension" 11 | ) 12 | 13 | var extensionAssetBundleCmd = &cobra.Command{ 14 | Use: "build [path]", 15 | Short: "Builds assets for extensions", 16 | Args: cobra.MinimumNArgs(1), 17 | RunE: func(cmd *cobra.Command, args []string) error { 18 | assetCfg := extension.AssetBuildConfig{ 19 | ShopwareRoot: os.Getenv("SHOPWARE_PROJECT_ROOT"), 20 | } 21 | validatedExtensions := make([]extension.Extension, 0) 22 | 23 | for _, arg := range args { 24 | path, err := filepath.Abs(arg) 25 | if err != nil { 26 | return fmt.Errorf("cannot open file: %w", err) 27 | } 28 | 29 | ext, err := extension.GetExtensionByFolder(path) 30 | if err != nil { 31 | return fmt.Errorf("cannot open extension: %w", err) 32 | } 33 | 34 | validatedExtensions = append(validatedExtensions, ext) 35 | } 36 | 37 | if assetCfg.ShopwareRoot != "" { 38 | constraint, err := extension.GetShopwareProjectConstraint(assetCfg.ShopwareRoot) 39 | if err != nil { 40 | return fmt.Errorf("cannot get shopware version constraint from project %s: %w", assetCfg.ShopwareRoot, err) 41 | } 42 | assetCfg.ShopwareVersion = constraint 43 | } else { 44 | constraint, err := validatedExtensions[0].GetShopwareVersionConstraint() 45 | if err != nil { 46 | return fmt.Errorf("cannot get shopware version constraint: %w", err) 47 | } 48 | 49 | assetCfg.ShopwareVersion = constraint 50 | } 51 | 52 | if err := extension.BuildAssetsForExtensions(cmd.Context(), extension.ConvertExtensionsToSources(cmd.Context(), validatedExtensions), assetCfg); err != nil { 53 | return fmt.Errorf("cannot build assets: %w", err) 54 | } 55 | 56 | return nil 57 | }, 58 | } 59 | 60 | func init() { 61 | extensionRootCmd.AddCommand(extensionAssetBundleCmd) 62 | } 63 | -------------------------------------------------------------------------------- /cmd/extension/extension_changelog.go: -------------------------------------------------------------------------------- 1 | package extension 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | 9 | "github.com/spf13/cobra" 10 | 11 | "github.com/shopware/shopware-cli/extension" 12 | ) 13 | 14 | var extensionChangelogCmd = &cobra.Command{ 15 | Use: "get-changelog [path]", 16 | Short: "Get the changelog", 17 | Args: cobra.MinimumNArgs(1), 18 | RunE: func(cmd *cobra.Command, args []string) error { 19 | path, err := filepath.Abs(args[0]) 20 | if err != nil { 21 | return fmt.Errorf("cannot find path: %w", err) 22 | } 23 | 24 | stat, err := os.Stat(path) 25 | if err != nil { 26 | return fmt.Errorf("cannot find path: %w", err) 27 | } 28 | 29 | var ext extension.Extension 30 | 31 | if stat.IsDir() { 32 | ext, err = extension.GetExtensionByFolder(path) 33 | } else { 34 | ext, err = extension.GetExtensionByZip(path) 35 | } 36 | 37 | if err != nil { 38 | return fmt.Errorf("changelog: cannot open extension %w", err) 39 | } 40 | 41 | changelog, err := ext.GetChangelog() 42 | if err != nil { 43 | return fmt.Errorf("cannot generate changelog: %w", err) 44 | } 45 | 46 | requestedLanguage, _ := cmd.Flags().GetString("language") 47 | 48 | if requestedLanguage == "" { 49 | fmt.Println(changelog.English) 50 | return nil 51 | } 52 | 53 | langKeys := strings.Split(requestedLanguage, ",") 54 | 55 | for _, langKey := range langKeys { 56 | lang, ok := changelog.Changelogs[langKey] 57 | 58 | if ok { 59 | fmt.Println(lang) 60 | return nil 61 | } 62 | } 63 | 64 | return fmt.Errorf("changelog for language %s not found", requestedLanguage) 65 | }, 66 | } 67 | 68 | func init() { 69 | extensionRootCmd.AddCommand(extensionChangelogCmd) 70 | extensionChangelogCmd.PersistentFlags().String("language", "", "Language of the changelog, can be multiple specified as fallback (comma separated)") 71 | } 72 | -------------------------------------------------------------------------------- /cmd/extension/extension_fix.go: -------------------------------------------------------------------------------- 1 | package extension 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/spf13/cobra" 9 | "golang.org/x/sync/errgroup" 10 | 11 | "github.com/shopware/shopware-cli/extension" 12 | "github.com/shopware/shopware-cli/internal/verifier" 13 | "github.com/shopware/shopware-cli/logging" 14 | ) 15 | 16 | var extensionFixCmd = &cobra.Command{ 17 | Use: "fix [path]", 18 | Short: "Fix an extension", 19 | Args: cobra.MinimumNArgs(1), 20 | PreRunE: func(cmd *cobra.Command, args []string) error { 21 | return verifier.SetupTools(cmd.Context(), cmd.Root().Version) 22 | }, 23 | RunE: func(cmd *cobra.Command, args []string) error { 24 | allowNonGit, _ := cmd.Flags().GetBool("allow-non-git") 25 | 26 | if !allowNonGit { 27 | if stat, err := os.Stat(filepath.Join(args[0], ".git")); err != nil || !stat.IsDir() { 28 | return fmt.Errorf("provided folder is not a git repository. Use --allow-non-git flag to run anyway") 29 | } 30 | } 31 | 32 | path, err := filepath.Abs(args[0]) 33 | if err != nil { 34 | return fmt.Errorf("cannot find path: %w", err) 35 | } 36 | 37 | ext, err := extension.GetExtensionByFolder(path) 38 | if err != nil { 39 | return err 40 | } 41 | 42 | toolCfg, err := verifier.ConvertExtensionToToolConfig(ext) 43 | if err != nil { 44 | return err 45 | } 46 | 47 | logging.FromContext(cmd.Context()).Debugf("Running fixes for Shopware version: %s", toolCfg.MinShopwareVersion) 48 | 49 | var gr errgroup.Group 50 | 51 | tools := verifier.GetTools() 52 | only, _ := cmd.Flags().GetString("only") 53 | 54 | tools, err = tools.Only(only) 55 | if err != nil { 56 | return err 57 | } 58 | 59 | for _, tool := range tools { 60 | tool := tool 61 | gr.Go(func() error { 62 | return tool.Fix(cmd.Context(), *toolCfg) 63 | }) 64 | } 65 | 66 | if err := gr.Wait(); err != nil { 67 | return err 68 | } 69 | 70 | return nil 71 | }, 72 | } 73 | 74 | func init() { 75 | extensionRootCmd.AddCommand(extensionFixCmd) 76 | extensionFixCmd.Flags().String("only", "", "Run only specific tools by name (comma-separated, e.g. phpstan,eslint)") 77 | extensionFixCmd.Flags().Bool("allow-non-git", false, "Allow running the fix command on non-git repositories") 78 | } 79 | -------------------------------------------------------------------------------- /cmd/extension/extension_format.go: -------------------------------------------------------------------------------- 1 | package extension 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | 7 | "github.com/spf13/cobra" 8 | "golang.org/x/sync/errgroup" 9 | 10 | "github.com/shopware/shopware-cli/extension" 11 | "github.com/shopware/shopware-cli/internal/verifier" 12 | "github.com/shopware/shopware-cli/logging" 13 | ) 14 | 15 | var extensionFormat = &cobra.Command{ 16 | Use: "format", 17 | Short: "Format an extension", 18 | PreRunE: func(cmd *cobra.Command, args []string) error { 19 | return verifier.SetupTools(cmd.Context(), cmd.Root().Version) 20 | }, 21 | RunE: func(cmd *cobra.Command, args []string) error { 22 | dryRun, _ := cmd.Flags().GetBool("dry-run") 23 | 24 | path, err := filepath.Abs(args[0]) 25 | if err != nil { 26 | return fmt.Errorf("cannot find path: %w", err) 27 | } 28 | 29 | ext, err := extension.GetExtensionByFolder(path) 30 | if err != nil { 31 | return err 32 | } 33 | 34 | toolCfg, err := verifier.ConvertExtensionToToolConfig(ext) 35 | if err != nil { 36 | return err 37 | } 38 | 39 | logging.FromContext(cmd.Context()).Debugf("Running fixes for Shopware version: %s", toolCfg.MinShopwareVersion) 40 | 41 | var gr errgroup.Group 42 | 43 | tools := verifier.GetTools() 44 | only, _ := cmd.Flags().GetString("only") 45 | 46 | tools, err = tools.Only(only) 47 | if err != nil { 48 | return err 49 | } 50 | 51 | for _, tool := range tools { 52 | tool := tool 53 | gr.Go(func() error { 54 | return tool.Format(cmd.Context(), *toolCfg, dryRun) 55 | }) 56 | } 57 | 58 | if err := gr.Wait(); err != nil { 59 | return err 60 | } 61 | 62 | return nil 63 | }, 64 | } 65 | 66 | func init() { 67 | extensionRootCmd.AddCommand(extensionFormat) 68 | extensionFormat.Flags().String("only", "", "Run only specific tools by name (comma-separated, e.g. phpstan,eslint)") 69 | extensionFormat.Flags().Bool("dry-run", false, "Run in dry run mode") 70 | } 71 | -------------------------------------------------------------------------------- /cmd/extension/extension_prepare.go: -------------------------------------------------------------------------------- 1 | package extension 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | 7 | "github.com/spf13/cobra" 8 | 9 | "github.com/shopware/shopware-cli/extension" 10 | ) 11 | 12 | var extensionPrepareCmd = &cobra.Command{ 13 | Use: "prepare [path]", 14 | Short: "Install Composer dependencies of an extension and delete unnecessary files for zipping", 15 | Args: cobra.MinimumNArgs(1), 16 | RunE: func(cmd *cobra.Command, args []string) error { 17 | path, err := filepath.Abs(args[0]) 18 | if err != nil { 19 | return fmt.Errorf("path not found: %w", err) 20 | } 21 | 22 | ext, err := extension.GetExtensionByFolder(path) 23 | if err != nil { 24 | return fmt.Errorf("detect extension type: %w", err) 25 | } 26 | 27 | err = extension.PrepareFolderForZipping(cmd.Context(), path+"/", ext, ext.GetExtensionConfig()) 28 | if err != nil { 29 | return fmt.Errorf("prepare zip: %w", err) 30 | } 31 | 32 | return nil 33 | }, 34 | } 35 | 36 | func init() { 37 | extensionRootCmd.AddCommand(extensionPrepareCmd) 38 | } 39 | -------------------------------------------------------------------------------- /cmd/extension/extension_version.go: -------------------------------------------------------------------------------- 1 | package extension 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/spf13/cobra" 9 | 10 | "github.com/shopware/shopware-cli/extension" 11 | ) 12 | 13 | var extensionVersionCmd = &cobra.Command{ 14 | Use: "get-version [path]", 15 | Short: "Get the version of the given extension", 16 | Args: cobra.MinimumNArgs(1), 17 | RunE: func(cmd *cobra.Command, args []string) error { 18 | path, err := filepath.Abs(args[0]) 19 | if err != nil { 20 | return fmt.Errorf("cannot find path: %w", err) 21 | } 22 | 23 | stat, err := os.Stat(path) 24 | if err != nil { 25 | return fmt.Errorf("cannot find path: %w", err) 26 | } 27 | 28 | var ext extension.Extension 29 | 30 | if stat.IsDir() { 31 | ext, err = extension.GetExtensionByFolder(path) 32 | } else { 33 | ext, err = extension.GetExtensionByZip(path) 34 | } 35 | 36 | if err != nil { 37 | return fmt.Errorf("version: cannot open extension %w", err) 38 | } 39 | 40 | version, err := ext.GetVersion() 41 | if err != nil { 42 | return fmt.Errorf("cannot generate version: %w", err) 43 | } 44 | 45 | fmt.Println(version.String()) 46 | 47 | return nil 48 | }, 49 | } 50 | 51 | func init() { 52 | extensionRootCmd.AddCommand(extensionVersionCmd) 53 | } 54 | -------------------------------------------------------------------------------- /cmd/extension/static/live-reload.js: -------------------------------------------------------------------------------- 1 | let bundles; 2 | if (Shopware.State !== undefined && Shopware.State.get('context') !== undefined) { 3 | bundles = Shopware.State.get('context').app.config.bundles; 4 | } else { 5 | bundles = Shopware.Store.get('context').app.config.bundles; 6 | } 7 | 8 | for (const bundleName of Object.keys(bundles)) { 9 | const bundle = bundles[bundleName]; 10 | 11 | if (bundle.liveReload !== true) { 12 | continue; 13 | } 14 | 15 | new EventSource(`/.shopware-cli/${bundle.name}/esbuild`).addEventListener('change', e => { 16 | const { added, removed, updated } = JSON.parse(e.data) 17 | 18 | // patch the path of esbuild 19 | updated[0] = `/.shopware-cli/${bundle.name}${updated[0]}` 20 | 21 | if (!added.length && !removed.length && updated.length === 1) { 22 | for (const link of document.getElementsByTagName("link")) { 23 | const url = new URL(link.href) 24 | 25 | if (url.host === location.host && url.pathname === updated[0]) { 26 | const next = link.cloneNode() 27 | next.href = updated[0] + '?' + Math.random().toString(36).slice(2) 28 | next.onload = () => link.remove() 29 | link.parentNode.insertBefore(next, link.nextSibling) 30 | return 31 | } 32 | } 33 | } 34 | 35 | location.reload() 36 | }) 37 | } -------------------------------------------------------------------------------- /cmd/project/ci_test.go: -------------------------------------------------------------------------------- 1 | package project 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestSourceMapCleanup(t *testing.T) { 11 | t.Run("invalid directory", func(t *testing.T) { 12 | assert.NoError(t, cleanupJavaScriptSourceMaps("invalid-directory")) 13 | }) 14 | 15 | t.Run("does not touch js", func(t *testing.T) { 16 | tmpDir := t.TempDir() 17 | 18 | assert.NoError(t, cleanupJavaScriptSourceMaps(tmpDir)) 19 | 20 | assert.NoError(t, os.WriteFile(tmpDir+"/random.js", []byte("test"), 0o644)) 21 | 22 | assert.NoError(t, cleanupJavaScriptSourceMaps(tmpDir)) 23 | 24 | assert.FileExists(t, tmpDir+"/random.js") 25 | }) 26 | 27 | t.Run("removes map files", func(t *testing.T) { 28 | tmpDir := t.TempDir() 29 | 30 | assert.NoError(t, os.WriteFile(tmpDir+"/foo.js.map", []byte("test"), 0o644)) 31 | 32 | assert.NoError(t, cleanupJavaScriptSourceMaps(tmpDir)) 33 | 34 | assert.NoFileExists(t, tmpDir+"/foo.js.map") 35 | }) 36 | 37 | t.Run("remove sourcemap comments", func(t *testing.T) { 38 | tmpDir := t.TempDir() 39 | 40 | assert.NoError(t, os.WriteFile(tmpDir+"/test.js", []byte("console.log//# sourceMappingURL=test.js.map"), 0o644)) 41 | assert.NoError(t, os.WriteFile(tmpDir+"/test.js.map", []byte("test"), 0o644)) 42 | 43 | assert.NoError(t, cleanupJavaScriptSourceMaps(tmpDir)) 44 | 45 | content, err := os.ReadFile(tmpDir + "/test.js") 46 | assert.NoError(t, err) 47 | 48 | assert.Equal(t, "console.log", string(content)) 49 | }) 50 | } 51 | -------------------------------------------------------------------------------- /cmd/project/config_sync_entity.go: -------------------------------------------------------------------------------- 1 | package project 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | 8 | adminSdk "github.com/friendsofshopware/go-shopware-admin-api-sdk" 9 | 10 | "github.com/shopware/shopware-cli/logging" 11 | "github.com/shopware/shopware-cli/shop" 12 | ) 13 | 14 | type EntitySync struct{} 15 | 16 | func (EntitySync) Push(ctx adminSdk.ApiContext, client *adminSdk.Client, config *shop.Config, operation *ConfigSyncOperation) error { 17 | for _, entity := range config.Sync.Entity { 18 | if entity.Exists != nil && len(*entity.Exists) > 0 { 19 | criteria := make(map[string]interface{}) 20 | criteria["filter"] = entity.Exists 21 | 22 | searchPayload, err := json.Marshal(criteria) 23 | if err != nil { 24 | return err 25 | } 26 | 27 | r, err := client.NewRequest(ctx, "POST", fmt.Sprintf("/api/search-ids/%s", entity.Entity), bytes.NewReader(searchPayload)) 28 | if err != nil { 29 | return err 30 | } 31 | 32 | r.Header.Set("Accept", "application/json") 33 | r.Header.Set("Content-Type", "application/json") 34 | 35 | var res criteriaApiResponse 36 | resp, err := client.Do(ctx.Context, r, &res) 37 | if err != nil { 38 | return err 39 | } 40 | 41 | defer func() { 42 | if err := resp.Body.Close(); err != nil { 43 | logging.FromContext(ctx.Context).Errorf("Push: %v", err) 44 | } 45 | }() 46 | 47 | if res.Total > 0 { 48 | continue 49 | } 50 | } 51 | 52 | operation.Operations[shop.NewUuid()] = adminSdk.SyncOperation{ 53 | Action: "upsert", 54 | Entity: entity.Entity, 55 | Payload: []map[string]interface{}{entity.Payload}, 56 | } 57 | } 58 | 59 | return nil 60 | } 61 | 62 | func (EntitySync) Pull(_ adminSdk.ApiContext, _ *adminSdk.Client, _ *shop.Config) error { 63 | return nil 64 | } 65 | 66 | type criteriaApiResponse struct { 67 | Total int `json:"total"` 68 | Data []string `json:"data"` 69 | } 70 | -------------------------------------------------------------------------------- /cmd/project/project.go: -------------------------------------------------------------------------------- 1 | package project 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | 6 | "github.com/shopware/shopware-cli/shop" 7 | ) 8 | 9 | var projectConfigPath string 10 | 11 | var projectRootCmd = &cobra.Command{ 12 | Use: "project", 13 | Short: "Manage your Shopware Project", 14 | } 15 | 16 | func Register(rootCmd *cobra.Command) { 17 | rootCmd.AddCommand(projectRootCmd) 18 | projectRootCmd.PersistentFlags().StringVar(&projectConfigPath, "project-config", shop.DefaultConfigFileName(), "Path to config") 19 | } 20 | -------------------------------------------------------------------------------- /cmd/project/project_autofix.go: -------------------------------------------------------------------------------- 1 | package project 2 | 3 | import "github.com/spf13/cobra" 4 | 5 | var projectAutofixCmd = &cobra.Command{ 6 | Use: "autofix", 7 | Short: "Autofix a project", 8 | } 9 | 10 | func init() { 11 | projectRootCmd.AddCommand(projectAutofixCmd) 12 | } 13 | -------------------------------------------------------------------------------- /cmd/project/project_autofix_flex.go: -------------------------------------------------------------------------------- 1 | package project 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path" 7 | 8 | "github.com/charmbracelet/huh" 9 | "github.com/spf13/cobra" 10 | 11 | "github.com/shopware/shopware-cli/internal/color" 12 | "github.com/shopware/shopware-cli/internal/flexmigrator" 13 | ) 14 | 15 | var projectAutofixFlexCmd = &cobra.Command{ 16 | Use: "flex", 17 | Short: "Autofix project to Symfony Flex", 18 | RunE: func(cmd *cobra.Command, args []string) error { 19 | project, err := findClosestShopwareProject() 20 | if err != nil { 21 | return err 22 | } 23 | 24 | var confirmed bool 25 | if err := huh.NewConfirm(). 26 | Title("Are you sure you want to autofix this project to Symfony Flex?"). 27 | Description("This will modify your composer.json and .env files. Make sure to commit your changes before running this command."). 28 | Value(&confirmed). 29 | Run(); err != nil { 30 | return err 31 | } 32 | 33 | if !confirmed { 34 | return fmt.Errorf("autofix cancelled") 35 | } 36 | 37 | if _, err := os.Stat(path.Join(project, "symfony.lock")); err == nil { 38 | return fmt.Errorf("symfony.lock already exists, is that project already migrated to Symfony Flex?") 39 | } 40 | 41 | if err := flexmigrator.MigrateComposerJson(project); err != nil { 42 | return err 43 | } 44 | 45 | if err := flexmigrator.MigrateEnv(project); err != nil { 46 | return err 47 | } 48 | 49 | if err := flexmigrator.Cleanup(project); err != nil { 50 | return err 51 | } 52 | 53 | fmt.Println("Project migrated to Symfony Flex") 54 | fmt.Printf("Please run %s to install the new dependencies\n", color.GreenText.Render("composer update")) 55 | fmt.Printf("and %s to apply the recipes\n", color.GreenText.Render("yes | composer recipes:install --reset --force")) 56 | 57 | return nil 58 | }, 59 | } 60 | 61 | func init() { 62 | projectAutofixCmd.AddCommand(projectAutofixFlexCmd) 63 | } 64 | -------------------------------------------------------------------------------- /cmd/project/project_clear_cache.go: -------------------------------------------------------------------------------- 1 | package project 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | adminSdk "github.com/friendsofshopware/go-shopware-admin-api-sdk" 8 | "github.com/spf13/cobra" 9 | 10 | "github.com/shopware/shopware-cli/logging" 11 | "github.com/shopware/shopware-cli/shop" 12 | ) 13 | 14 | var projectClearCacheCmd = &cobra.Command{ 15 | Use: "clear-cache", 16 | Short: "Clears the Shop cache", 17 | RunE: func(cmd *cobra.Command, _ []string) error { 18 | var cfg *shop.Config 19 | var err error 20 | 21 | if cfg, err = shop.ReadConfig(projectConfigPath, false); err != nil { 22 | return err 23 | } 24 | 25 | if cfg.AdminApi == nil { 26 | logging.FromContext(cmd.Context()).Infof("Clearing cache localy") 27 | 28 | projectRoot, err := findClosestShopwareProject() 29 | if err != nil { 30 | return err 31 | } 32 | 33 | return os.RemoveAll(fmt.Sprintf("%s/var/cache", projectRoot)) 34 | } 35 | 36 | logging.FromContext(cmd.Context()).Infof("Clearing cache using admin-api") 37 | 38 | client, err := shop.NewShopClient(cmd.Context(), cfg) 39 | if err != nil { 40 | return err 41 | } 42 | 43 | _, err = client.CacheManager.Clear(adminSdk.NewApiContext(cmd.Context())) 44 | 45 | return err 46 | }, 47 | } 48 | 49 | func init() { 50 | projectRootCmd.AddCommand(projectClearCacheCmd) 51 | } 52 | -------------------------------------------------------------------------------- /cmd/project/project_config.go: -------------------------------------------------------------------------------- 1 | package project 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | var projectConfigCmd = &cobra.Command{ 8 | Use: "config", 9 | Short: "Manage the project config", 10 | } 11 | 12 | func init() { 13 | projectRootCmd.AddCommand(projectConfigCmd) 14 | } 15 | -------------------------------------------------------------------------------- /cmd/project/project_config_pull.go: -------------------------------------------------------------------------------- 1 | package project 2 | 3 | import ( 4 | "os" 5 | 6 | adminSdk "github.com/friendsofshopware/go-shopware-admin-api-sdk" 7 | "github.com/spf13/cobra" 8 | "gopkg.in/yaml.v3" 9 | 10 | "github.com/shopware/shopware-cli/logging" 11 | "github.com/shopware/shopware-cli/shop" 12 | ) 13 | 14 | var projectConfigPullCmd = &cobra.Command{ 15 | Use: "pull", 16 | Short: "Synchronizes your shop config to local", 17 | RunE: func(cmd *cobra.Command, _ []string) error { 18 | var cfg *shop.Config 19 | var err error 20 | 21 | if cfg, err = shop.ReadConfig(projectConfigPath, false); err != nil { 22 | return err 23 | } 24 | 25 | client, err := shop.NewShopClient(cmd.Context(), cfg) 26 | if err != nil { 27 | return err 28 | } 29 | 30 | for _, applyer := range NewSyncApplyers(cfg) { 31 | if err := applyer.Pull(adminSdk.NewApiContext(cmd.Context()), client, cfg); err != nil { 32 | return err 33 | } 34 | } 35 | 36 | content, err := yaml.Marshal(cfg) 37 | if err != nil { 38 | return err 39 | } 40 | 41 | if err := os.WriteFile(projectConfigPath, content, os.ModePerm); err != nil { 42 | return err 43 | } 44 | 45 | logging.FromContext(cmd.Context()).Infof("%s has been updated", projectConfigPath) 46 | 47 | return nil 48 | }, 49 | } 50 | 51 | func init() { 52 | projectConfigCmd.AddCommand(projectConfigPullCmd) 53 | } 54 | -------------------------------------------------------------------------------- /cmd/project/project_debug.go: -------------------------------------------------------------------------------- 1 | package project 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/spf13/cobra" 9 | 10 | "github.com/shopware/shopware-cli/extension" 11 | "github.com/shopware/shopware-cli/internal/table" 12 | "github.com/shopware/shopware-cli/logging" 13 | "github.com/shopware/shopware-cli/shop" 14 | ) 15 | 16 | var projectDebug = &cobra.Command{ 17 | Use: "debug", 18 | Short: "Shows detected Shopware version and detected extensions for further debugging", 19 | Args: cobra.ExactArgs(1), 20 | RunE: func(cmd *cobra.Command, args []string) error { 21 | var err error 22 | args[0], err = filepath.Abs(args[0]) 23 | if err != nil { 24 | return err 25 | } 26 | 27 | shopCfg, err := shop.ReadConfig(projectConfigPath, true) 28 | if err != nil { 29 | return err 30 | } 31 | 32 | shopwareConstraint, err := extension.GetShopwareProjectConstraint(args[0]) 33 | if err != nil { 34 | return err 35 | } 36 | 37 | if shopCfg.IsFallback() { 38 | fmt.Printf("Could not find a %s, using fallback config\n", projectConfigPath) 39 | } else { 40 | fmt.Printf("Found config: Yes\n") 41 | } 42 | fmt.Printf("Detected following Shopware version: %s\n", shopwareConstraint.String()) 43 | 44 | sources := extension.FindAssetSourcesOfProject(logging.DisableLogger(cmd.Context()), args[0], shopCfg) 45 | 46 | fmt.Println("Following extensions/bundles has been detected") 47 | table := table.NewWriter(os.Stdout) 48 | table.Header([]string{"Name", "Path"}) 49 | 50 | for _, source := range sources { 51 | _ = table.Append([]string{source.Name, source.Path}) 52 | } 53 | 54 | _ = table.Render() 55 | 56 | return nil 57 | }, 58 | } 59 | 60 | func init() { 61 | projectRootCmd.AddCommand(projectDebug) 62 | } 63 | -------------------------------------------------------------------------------- /cmd/project/project_extension.go: -------------------------------------------------------------------------------- 1 | package project 2 | 3 | import "github.com/spf13/cobra" 4 | 5 | var projectExtensionCmd = &cobra.Command{ 6 | Use: "extension", 7 | Short: "Manage the extensions of the Shopware shop", 8 | } 9 | 10 | func init() { 11 | projectRootCmd.AddCommand(projectExtensionCmd) 12 | } 13 | -------------------------------------------------------------------------------- /cmd/project/project_extension_activate.go: -------------------------------------------------------------------------------- 1 | package project 2 | 3 | import ( 4 | "fmt" 5 | 6 | adminSdk "github.com/friendsofshopware/go-shopware-admin-api-sdk" 7 | "github.com/spf13/cobra" 8 | 9 | "github.com/shopware/shopware-cli/logging" 10 | "github.com/shopware/shopware-cli/shop" 11 | ) 12 | 13 | var projectExtensionActivateCmd = &cobra.Command{ 14 | Use: "activate [name]", 15 | Short: "Activate a extension", 16 | Args: cobra.MinimumNArgs(1), 17 | RunE: func(cmd *cobra.Command, args []string) error { 18 | var cfg *shop.Config 19 | var err error 20 | 21 | if cfg, err = shop.ReadConfig(projectConfigPath, false); err != nil { 22 | return err 23 | } 24 | 25 | client, err := shop.NewShopClient(cmd.Context(), cfg) 26 | if err != nil { 27 | return err 28 | } 29 | 30 | extensions, _, err := client.ExtensionManager.ListAvailableExtensions(adminSdk.NewApiContext(cmd.Context())) 31 | if err != nil { 32 | return err 33 | } 34 | 35 | failed := false 36 | 37 | for _, arg := range args { 38 | extension := extensions.GetByName(arg) 39 | 40 | if extension == nil { 41 | failed = true 42 | logging.FromContext(cmd.Context()).Errorf("Cannot find extension by name %s", arg) 43 | continue 44 | } 45 | 46 | if extension.Active { 47 | logging.FromContext(cmd.Context()).Infof("Extension %s is already active", arg) 48 | continue 49 | } 50 | 51 | if extension.InstalledAt == nil { 52 | if _, err := client.ExtensionManager.InstallExtension(adminSdk.NewApiContext(cmd.Context()), extension.Type, extension.Name); err != nil { 53 | failed = true 54 | 55 | logging.FromContext(cmd.Context()).Errorf("Installation of %s failed with error: %v", extension.Name, err) 56 | } 57 | } 58 | 59 | if _, err := client.ExtensionManager.ActivateExtension(adminSdk.NewApiContext(cmd.Context()), extension.Type, extension.Name); err != nil { 60 | failed = true 61 | 62 | logging.FromContext(cmd.Context()).Errorf("Activate of %s failed with error: %v", extension.Name, err) 63 | } 64 | 65 | logging.FromContext(cmd.Context()).Infof("Activated %s", extension.Name) 66 | } 67 | 68 | if failed { 69 | return fmt.Errorf("activation failed") 70 | } 71 | 72 | return nil 73 | }, 74 | } 75 | 76 | func init() { 77 | projectExtensionCmd.AddCommand(projectExtensionActivateCmd) 78 | } 79 | -------------------------------------------------------------------------------- /cmd/project/project_extension_deactivate.go: -------------------------------------------------------------------------------- 1 | package project 2 | 3 | import ( 4 | "fmt" 5 | 6 | adminSdk "github.com/friendsofshopware/go-shopware-admin-api-sdk" 7 | "github.com/spf13/cobra" 8 | 9 | "github.com/shopware/shopware-cli/logging" 10 | "github.com/shopware/shopware-cli/shop" 11 | ) 12 | 13 | var projectExtensionDeactivateCmd = &cobra.Command{ 14 | Use: "deactivate [name]", 15 | Short: "Deactivate a extension", 16 | Args: cobra.MinimumNArgs(1), 17 | RunE: func(cmd *cobra.Command, args []string) error { 18 | var cfg *shop.Config 19 | var err error 20 | 21 | if cfg, err = shop.ReadConfig(projectConfigPath, false); err != nil { 22 | return err 23 | } 24 | 25 | client, err := shop.NewShopClient(cmd.Context(), cfg) 26 | if err != nil { 27 | return err 28 | } 29 | 30 | extensions, _, err := client.ExtensionManager.ListAvailableExtensions(adminSdk.NewApiContext(cmd.Context())) 31 | if err != nil { 32 | return err 33 | } 34 | 35 | failed := false 36 | 37 | for _, arg := range args { 38 | extension := extensions.GetByName(arg) 39 | 40 | if extension == nil { 41 | failed = true 42 | logging.FromContext(cmd.Context()).Errorf("Cannot find extension by name %s", arg) 43 | continue 44 | } 45 | 46 | if !extension.Active { 47 | logging.FromContext(cmd.Context()).Infof("Extension %s is already deactivated", arg) 48 | continue 49 | } 50 | 51 | if _, err := client.ExtensionManager.DeactivateExtension(adminSdk.NewApiContext(cmd.Context()), extension.Type, extension.Name); err != nil { 52 | failed = true 53 | 54 | logging.FromContext(cmd.Context()).Errorf("Deactivation of %s failed with error: %v", extension.Name, err) 55 | } 56 | 57 | logging.FromContext(cmd.Context()).Infof("Deactivated %s", extension.Name) 58 | } 59 | 60 | if failed { 61 | return fmt.Errorf("deactivation failed") 62 | } 63 | 64 | return nil 65 | }, 66 | } 67 | 68 | func init() { 69 | projectExtensionCmd.AddCommand(projectExtensionDeactivateCmd) 70 | } 71 | -------------------------------------------------------------------------------- /cmd/project/project_extension_list.go: -------------------------------------------------------------------------------- 1 | package project 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | 8 | adminSdk "github.com/friendsofshopware/go-shopware-admin-api-sdk" 9 | "github.com/spf13/cobra" 10 | 11 | "github.com/shopware/shopware-cli/internal/table" 12 | "github.com/shopware/shopware-cli/shop" 13 | ) 14 | 15 | var projectExtensionListCmd = &cobra.Command{ 16 | Use: "list", 17 | Aliases: []string{"ls"}, 18 | Short: "List all installed extensions", 19 | RunE: func(cmd *cobra.Command, _ []string) error { 20 | var cfg *shop.Config 21 | var err error 22 | 23 | outputAsJson, _ := cmd.PersistentFlags().GetBool("json") 24 | 25 | if cfg, err = shop.ReadConfig(projectConfigPath, false); err != nil { 26 | return err 27 | } 28 | 29 | client, err := shop.NewShopClient(cmd.Context(), cfg) 30 | if err != nil { 31 | return err 32 | } 33 | 34 | if _, err := client.ExtensionManager.Refresh(adminSdk.NewApiContext(cmd.Context())); err != nil { 35 | return err 36 | } 37 | 38 | extensions, _, err := client.ExtensionManager.ListAvailableExtensions(adminSdk.NewApiContext(cmd.Context())) 39 | if err != nil { 40 | return err 41 | } 42 | 43 | if outputAsJson { 44 | content, err := json.Marshal(extensions) 45 | if err != nil { 46 | return err 47 | } 48 | 49 | fmt.Println(string(content)) 50 | 51 | return nil 52 | } 53 | 54 | table := table.NewWriter(os.Stdout) 55 | table.Header([]string{"Name", "Version", "Status"}) 56 | 57 | for _, extension := range extensions { 58 | _ = table.Append([]string{extension.Name, extension.Version, extension.Status()}) 59 | } 60 | 61 | _ = table.Render() 62 | 63 | return nil 64 | }, 65 | } 66 | 67 | func init() { 68 | projectExtensionCmd.AddCommand(projectExtensionListCmd) 69 | projectExtensionListCmd.PersistentFlags().Bool("json", false, "Output as json") 70 | } 71 | -------------------------------------------------------------------------------- /cmd/project/project_extension_outdated.go: -------------------------------------------------------------------------------- 1 | package project 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | 8 | adminSdk "github.com/friendsofshopware/go-shopware-admin-api-sdk" 9 | "github.com/spf13/cobra" 10 | 11 | "github.com/shopware/shopware-cli/internal/table" 12 | "github.com/shopware/shopware-cli/logging" 13 | "github.com/shopware/shopware-cli/shop" 14 | ) 15 | 16 | var projectExtensionOutdatedCmd = &cobra.Command{ 17 | Use: "outdated", 18 | Short: "List all outdated extensions", 19 | RunE: func(cmd *cobra.Command, _ []string) error { 20 | var cfg *shop.Config 21 | var err error 22 | 23 | outputAsJson, _ := cmd.PersistentFlags().GetBool("json") 24 | 25 | if cfg, err = shop.ReadConfig(projectConfigPath, false); err != nil { 26 | return err 27 | } 28 | 29 | client, err := shop.NewShopClient(cmd.Context(), cfg) 30 | if err != nil { 31 | return err 32 | } 33 | 34 | if _, err := client.ExtensionManager.Refresh(adminSdk.NewApiContext(cmd.Context())); err != nil { 35 | return err 36 | } 37 | 38 | extensions, _, err := client.ExtensionManager.ListAvailableExtensions(adminSdk.NewApiContext(cmd.Context())) 39 | extensions = extensions.FilterByUpdateable() 40 | 41 | if err != nil { 42 | return err 43 | } 44 | 45 | if outputAsJson { 46 | content, err := json.Marshal(extensions) 47 | if err != nil { 48 | return err 49 | } 50 | 51 | fmt.Println(string(content)) 52 | 53 | return nil 54 | } 55 | 56 | if len(extensions) == 0 { 57 | logging.FromContext(cmd.Context()).Infof("All extensions are up-to-date") 58 | return nil 59 | } 60 | 61 | table := table.NewWriter(os.Stdout) 62 | table.Header([]string{"Name", "Current Version", "Latest Version", "Update Source"}) 63 | 64 | for _, extension := range extensions { 65 | _ = table.Append([]string{extension.Name, extension.Version, extension.LatestVersion, extension.UpdateSource}) 66 | } 67 | 68 | _ = table.Render() 69 | 70 | return fmt.Errorf("there are %d outdated extensions", len(extensions)) 71 | }, 72 | } 73 | 74 | func init() { 75 | projectExtensionCmd.AddCommand(projectExtensionOutdatedCmd) 76 | projectExtensionOutdatedCmd.PersistentFlags().Bool("json", false, "Output as json") 77 | } 78 | -------------------------------------------------------------------------------- /cmd/project/project_fix.go: -------------------------------------------------------------------------------- 1 | package project 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/spf13/cobra" 9 | "golang.org/x/sync/errgroup" 10 | 11 | "github.com/shopware/shopware-cli/internal/verifier" 12 | ) 13 | 14 | var projectFixCmd = &cobra.Command{ 15 | Use: "fix", 16 | Short: "Fix project", 17 | PreRunE: func(cmd *cobra.Command, args []string) error { 18 | return verifier.SetupTools(cmd.Context(), cmd.Root().Version) 19 | }, 20 | RunE: func(cmd *cobra.Command, args []string) error { 21 | allowNonGit, _ := cmd.Flags().GetBool("allow-non-git") 22 | gitPath := filepath.Join(args[0], ".git") 23 | if !allowNonGit { 24 | if stat, err := os.Stat(gitPath); err != nil || !stat.IsDir() { 25 | return fmt.Errorf("provided folder is not a git repository. Use --allow-non-git flag to run anyway") 26 | } 27 | } 28 | 29 | var err error 30 | only, _ := cmd.Flags().GetString("only") 31 | 32 | projectPath := "" 33 | 34 | if len(args) > 0 { 35 | projectPath = args[0] 36 | } else { 37 | projectPath, err = findClosestShopwareProject() 38 | if err != nil { 39 | return err 40 | } 41 | } 42 | 43 | projectPath, err = filepath.Abs(projectPath) 44 | if err != nil { 45 | return fmt.Errorf("cannot find path: %w", err) 46 | } 47 | 48 | toolCfg, err := verifier.GetConfigFromProject(projectPath) 49 | if err != nil { 50 | return err 51 | } 52 | 53 | var gr errgroup.Group 54 | 55 | tools := verifier.GetTools() 56 | 57 | tools, err = tools.Only(only) 58 | if err != nil { 59 | return err 60 | } 61 | 62 | for _, tool := range tools { 63 | tool := tool 64 | gr.Go(func() error { 65 | return tool.Fix(cmd.Context(), *toolCfg) 66 | }) 67 | } 68 | 69 | return gr.Wait() 70 | }, 71 | } 72 | 73 | func init() { 74 | projectRootCmd.AddCommand(projectFixCmd) 75 | projectFixCmd.PersistentFlags().String("only", "", "Run only specific tools by name (comma-separated, e.g. phpstan,eslint)") 76 | projectFixCmd.PersistentFlags().Bool("allow-non-git", false, "Allow running on non git repositories") 77 | } 78 | -------------------------------------------------------------------------------- /cmd/project/project_format.go: -------------------------------------------------------------------------------- 1 | package project 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | 7 | "github.com/spf13/cobra" 8 | "golang.org/x/sync/errgroup" 9 | 10 | "github.com/shopware/shopware-cli/internal/verifier" 11 | ) 12 | 13 | var projectFormatCmd = &cobra.Command{ 14 | Use: "format", 15 | Short: "Format project", 16 | PreRunE: func(cmd *cobra.Command, args []string) error { 17 | return verifier.SetupTools(cmd.Context(), cmd.Root().Version) 18 | }, 19 | RunE: func(cmd *cobra.Command, args []string) error { 20 | var err error 21 | only, _ := cmd.Flags().GetString("only") 22 | dryRun, _ := cmd.Flags().GetBool("dry-run") 23 | 24 | projectPath := "" 25 | 26 | if len(args) > 0 { 27 | projectPath = args[0] 28 | } else { 29 | projectPath, err = findClosestShopwareProject() 30 | if err != nil { 31 | return err 32 | } 33 | } 34 | 35 | projectPath, err = filepath.Abs(projectPath) 36 | if err != nil { 37 | return fmt.Errorf("cannot find path: %w", err) 38 | } 39 | 40 | toolCfg, err := verifier.GetConfigFromProject(projectPath) 41 | if err != nil { 42 | return err 43 | } 44 | 45 | var gr errgroup.Group 46 | 47 | tools := verifier.GetTools() 48 | 49 | tools, err = tools.Only(only) 50 | if err != nil { 51 | return err 52 | } 53 | 54 | for _, tool := range tools { 55 | tool := tool 56 | gr.Go(func() error { 57 | return tool.Format(cmd.Context(), *toolCfg, dryRun) 58 | }) 59 | } 60 | 61 | return gr.Wait() 62 | }, 63 | } 64 | 65 | func init() { 66 | projectRootCmd.AddCommand(projectFormatCmd) 67 | projectFormatCmd.PersistentFlags().String("only", "", "Run only specific tools by name (comma-separated, e.g. phpstan,eslint)") 68 | projectFormatCmd.PersistentFlags().Bool("dry-run", false, "Run tools in dry run mode") 69 | } 70 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "slices" 7 | 8 | "github.com/spf13/cobra" 9 | 10 | "github.com/shopware/shopware-cli/cmd/account" 11 | "github.com/shopware/shopware-cli/cmd/extension" 12 | "github.com/shopware/shopware-cli/cmd/project" 13 | accountApi "github.com/shopware/shopware-cli/internal/account-api" 14 | "github.com/shopware/shopware-cli/internal/config" 15 | "github.com/shopware/shopware-cli/logging" 16 | ) 17 | 18 | var ( 19 | cfgFile string 20 | version = "dev" 21 | ) 22 | 23 | var rootCmd = &cobra.Command{ 24 | Use: "shopware-cli", 25 | Short: "A cli for common Shopware tasks", 26 | Long: `This application contains some utilities like extension management`, 27 | Version: version, 28 | } 29 | 30 | func Execute(ctx context.Context) { 31 | ctx = logging.WithLogger(ctx, logging.NewLogger(slices.Contains(os.Args, "--verbose"))) 32 | accountApi.SetUserAgent("shopware-cli/" + version) 33 | 34 | if err := rootCmd.ExecuteContext(ctx); err != nil { 35 | logging.FromContext(ctx).Fatalln(err) 36 | } 37 | } 38 | 39 | func init() { 40 | rootCmd.SilenceErrors = true 41 | 42 | cobra.OnInitialize(func() { 43 | _ = config.InitConfig(cfgFile) 44 | }) 45 | 46 | rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.shopware-cli.yaml)") 47 | rootCmd.PersistentFlags().Bool("verbose", false, "show debug output") 48 | 49 | project.Register(rootCmd) 50 | extension.Register(rootCmd) 51 | account.Register(rootCmd, func(commandName string) (*account.ServiceContainer, error) { 52 | err := config.InitConfig(cfgFile) 53 | if err != nil { 54 | return nil, err 55 | } 56 | conf := config.Config{} 57 | if commandName == "login" || commandName == "logout" { 58 | return &account.ServiceContainer{ 59 | Conf: conf, 60 | AccountClient: nil, 61 | }, nil 62 | } 63 | client, err := accountApi.NewApi(rootCmd.Context(), conf) 64 | if err != nil { 65 | return nil, err 66 | } 67 | return &account.ServiceContainer{ 68 | Conf: conf, 69 | AccountClient: client, 70 | }, nil 71 | }) 72 | } 73 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | -------------------------------------------------------------------------------- /extension/asset.go: -------------------------------------------------------------------------------- 1 | package extension 2 | 3 | import ( 4 | "context" 5 | "path" 6 | "path/filepath" 7 | 8 | "github.com/shopware/shopware-cli/internal/asset" 9 | "github.com/shopware/shopware-cli/logging" 10 | ) 11 | 12 | func ConvertExtensionsToSources(ctx context.Context, extensions []Extension) []asset.Source { 13 | sources := make([]asset.Source, 0) 14 | 15 | for _, ext := range extensions { 16 | name, err := ext.GetName() 17 | if err != nil { 18 | logging.FromContext(ctx).Errorf("Skipping extension %s as it has a invalid name", ext.GetPath()) 19 | continue 20 | } 21 | 22 | sources = append(sources, asset.Source{ 23 | Name: name, 24 | Path: ext.GetRootDir(), 25 | AdminEsbuildCompatible: ext.GetExtensionConfig().Build.Zip.Assets.EnableESBuildForAdmin, 26 | StorefrontEsbuildCompatible: ext.GetExtensionConfig().Build.Zip.Assets.EnableESBuildForStorefront, 27 | DisableSass: ext.GetExtensionConfig().Build.Zip.Assets.DisableSass, 28 | NpmStrict: ext.GetExtensionConfig().Build.Zip.Assets.NpmStrict, 29 | }) 30 | 31 | extConfig := ext.GetExtensionConfig() 32 | 33 | if extConfig != nil { 34 | for _, bundle := range extConfig.Build.ExtraBundles { 35 | bundleName := bundle.Name 36 | 37 | if bundleName == "" { 38 | bundleName = filepath.Base(bundle.Path) 39 | } 40 | 41 | sources = append(sources, asset.Source{ 42 | Name: bundleName, 43 | Path: path.Join(ext.GetRootDir(), bundle.Path), 44 | AdminEsbuildCompatible: ext.GetExtensionConfig().Build.Zip.Assets.EnableESBuildForAdmin, 45 | StorefrontEsbuildCompatible: ext.GetExtensionConfig().Build.Zip.Assets.EnableESBuildForStorefront, 46 | DisableSass: ext.GetExtensionConfig().Build.Zip.Assets.DisableSass, 47 | NpmStrict: ext.GetExtensionConfig().Build.Zip.Assets.NpmStrict, 48 | }) 49 | } 50 | } 51 | } 52 | 53 | return sources 54 | } 55 | -------------------------------------------------------------------------------- /extension/build_modifier_test.go: -------------------------------------------------------------------------------- 1 | package extension 2 | 3 | import ( 4 | "encoding/xml" 5 | "os" 6 | "path/filepath" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | const exampleManifest = ` 13 | 14 | 15 | 1.0.0 16 | 17 | 18 | http://localhost/foo 19 | 20 | ` 21 | 22 | func TestSetVersionApp(t *testing.T) { 23 | app := &App{} 24 | 25 | tmpDir := t.TempDir() 26 | 27 | assert.NoError(t, os.WriteFile(filepath.Join(tmpDir, "manifest.xml"), []byte(exampleManifest), 0644)) 28 | 29 | assert.NoError(t, BuildModifier(app, tmpDir, BuildModifierConfig{Version: "5.0.0"})) 30 | 31 | bytes, err := os.ReadFile(filepath.Join(tmpDir, "manifest.xml")) 32 | 33 | assert.NoError(t, err) 34 | 35 | var manifest Manifest 36 | 37 | assert.NoError(t, xml.Unmarshal(bytes, &manifest)) 38 | 39 | assert.Equal(t, "5.0.0", manifest.Meta.Version) 40 | } 41 | 42 | func TestSetRegistration(t *testing.T) { 43 | app := &App{} 44 | 45 | tmpDir := t.TempDir() 46 | 47 | assert.NoError(t, os.WriteFile(filepath.Join(tmpDir, "manifest.xml"), []byte(exampleManifest), 0644)) 48 | 49 | assert.NoError(t, BuildModifier(app, tmpDir, BuildModifierConfig{AppBackendUrl: "https://foo.com"})) 50 | 51 | bytes, err := os.ReadFile(filepath.Join(tmpDir, "manifest.xml")) 52 | 53 | assert.NoError(t, err) 54 | 55 | var manifest Manifest 56 | 57 | assert.NoError(t, xml.Unmarshal(bytes, &manifest)) 58 | 59 | assert.Equal(t, "https://foo.com/foo", manifest.Setup.RegistrationUrl) 60 | } 61 | 62 | func TestSetRegistrationSecret(t *testing.T) { 63 | app := &App{} 64 | 65 | tmpDir := t.TempDir() 66 | 67 | assert.NoError(t, os.WriteFile(filepath.Join(tmpDir, "manifest.xml"), []byte(exampleManifest), 0644)) 68 | 69 | assert.NoError(t, BuildModifier(app, tmpDir, BuildModifierConfig{AppBackendSecret: "secret"})) 70 | 71 | bytes, err := os.ReadFile(filepath.Join(tmpDir, "manifest.xml")) 72 | 73 | assert.NoError(t, err) 74 | 75 | var manifest Manifest 76 | 77 | assert.NoError(t, xml.Unmarshal(bytes, &manifest)) 78 | 79 | assert.Equal(t, "http://localhost/foo", manifest.Setup.RegistrationUrl) 80 | assert.Equal(t, "secret", manifest.Setup.Secret) 81 | } 82 | -------------------------------------------------------------------------------- /extension/bun_helper.go: -------------------------------------------------------------------------------- 1 | package extension 2 | 3 | type NpmPackage struct { 4 | Dependencies map[string]string `json:"dependencies"` 5 | DevDependencies map[string]string `json:"devDependencies"` 6 | Scripts map[string]string `json:"scripts"` 7 | } 8 | 9 | func (p NpmPackage) HasScript(name string) bool { 10 | _, ok := p.Scripts[name] 11 | return ok 12 | } 13 | 14 | // When a package is defined in both dependencies and devDependencies, bun will crash. 15 | func canRunBunOnPackage(npmPackage NpmPackage) bool { 16 | for name := range npmPackage.Dependencies { 17 | if _, ok := npmPackage.DevDependencies[name]; ok { 18 | return false 19 | } 20 | } 21 | 22 | return true 23 | } 24 | -------------------------------------------------------------------------------- /extension/bun_helper_test.go: -------------------------------------------------------------------------------- 1 | package extension 2 | 3 | import ( 4 | "os" 5 | "path" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestValidPackageJsonBun(t *testing.T) { 12 | tmpDir := t.TempDir() 13 | 14 | packageJson := `{ 15 | "dependencies": { 16 | "foo": "1.0.0" 17 | } 18 | }` 19 | 20 | if err := os.WriteFile(path.Join(tmpDir, "package.json"), []byte(packageJson), 0644); err != nil { 21 | t.Fatal(err) 22 | } 23 | 24 | npm, err := getNpmPackage(tmpDir) 25 | 26 | assert.NoError(t, err) 27 | 28 | assert.True(t, canRunBunOnPackage(npm)) 29 | } 30 | 31 | func TestValidPackageJsonWithDevBun(t *testing.T) { 32 | tmpDir := t.TempDir() 33 | 34 | packageJson := `{ 35 | "dependencies": { 36 | "foo": "1.0.0" 37 | }, 38 | "devDependencies": { 39 | "bar": "1.0.0" 40 | } 41 | }` 42 | 43 | if err := os.WriteFile(path.Join(tmpDir, "package.json"), []byte(packageJson), 0644); err != nil { 44 | t.Fatal(err) 45 | } 46 | 47 | npm, err := getNpmPackage(tmpDir) 48 | 49 | assert.NoError(t, err) 50 | 51 | assert.True(t, canRunBunOnPackage(npm)) 52 | } 53 | 54 | func TestInvalidPackageJsonBun(t *testing.T) { 55 | tmpDir := t.TempDir() 56 | 57 | packageJson := `{ 58 | "dependencies": { 59 | "foo": "1.0.0" 60 | }, 61 | "devDependencies": { 62 | "foo": "1.0.0" 63 | } 64 | }` 65 | 66 | if err := os.WriteFile(path.Join(tmpDir, "package.json"), []byte(packageJson), 0644); err != nil { 67 | t.Fatal(err) 68 | } 69 | 70 | npm, err := getNpmPackage(tmpDir) 71 | 72 | assert.NoError(t, err) 73 | 74 | assert.False(t, canRunBunOnPackage(npm)) 75 | } 76 | -------------------------------------------------------------------------------- /extension/caniuse_update.go: -------------------------------------------------------------------------------- 1 | package extension 2 | 3 | import ( 4 | "encoding/json" 5 | "os" 6 | "strings" 7 | ) 8 | 9 | func patchPackageLockToRemoveCanIUsePackage(packageJsonPath string) error { 10 | body, err := os.ReadFile(packageJsonPath) 11 | 12 | if err != nil { 13 | return err 14 | } 15 | 16 | var lock map[string]interface{} 17 | 18 | if err := json.Unmarshal(body, &lock); err != nil { 19 | return err 20 | } 21 | 22 | if dependencies, ok := lock["dependencies"]; !ok { 23 | if mappedDeps, ok := dependencies.(map[string]interface{}); ok { 24 | delete(mappedDeps, "caniuse-lite") 25 | } 26 | } 27 | 28 | removeCanIUsePackage(lock) 29 | 30 | updatedBody, err := json.MarshalIndent(lock, "", " ") 31 | 32 | if err != nil { 33 | return err 34 | } 35 | 36 | return os.WriteFile(packageJsonPath, updatedBody, os.ModePerm) 37 | } 38 | 39 | func removeCanIUsePackage(pkg map[string]interface{}) { 40 | if dependencies, ok := pkg["dependencies"]; ok { 41 | if mappedDeps, ok := dependencies.(map[string]interface{}); ok { 42 | delete(mappedDeps, "caniuse-lite") 43 | 44 | for _, dep := range mappedDeps { 45 | if depMap, ok := dep.(map[string]interface{}); ok { 46 | removeCanIUsePackage(depMap) 47 | } 48 | } 49 | } 50 | } 51 | 52 | if packages, ok := pkg["packages"].(map[string]interface{}); ok { 53 | for name := range packages { 54 | if strings.HasSuffix(name, "caniuse-lite") { 55 | delete(packages, name) 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /extension/caniuse_update_test.go: -------------------------------------------------------------------------------- 1 | package extension 2 | 3 | import ( 4 | "os" 5 | "path" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestCanIUseUpdate(t *testing.T) { 12 | tmpDir := t.TempDir() 13 | 14 | packageLock := `{ 15 | "name": "test", 16 | "version": "1.0.0", 17 | "lockfileVersion": 3, 18 | "requires": true, 19 | "packages": { 20 | "": { 21 | "name": "test", 22 | "version": "1.0.0", 23 | "license": "ISC", 24 | "dependencies": { 25 | "caniuse-lite": "^1.0.30001570" 26 | } 27 | }, 28 | "node_modules/caniuse-lite": { 29 | "version": "1.0.30001570", 30 | "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001570.tgz", 31 | "integrity": "sha512-+3e0ASu4sw1SWaoCtvPeyXp+5PsjigkSt8OXZbF9StH5pQWbxEjLAZE3n8Aup5udop1uRiKA7a4utUk/uoSpUw==", 32 | "funding": [ 33 | { 34 | "type": "opencollective", 35 | "url": "https://opencollective.com/browserslist" 36 | }, 37 | { 38 | "type": "tidelift", 39 | "url": "https://tidelift.com/funding/github/npm/caniuse-lite" 40 | }, 41 | { 42 | "type": "github", 43 | "url": "https://github.com/sponsors/ai" 44 | } 45 | ] 46 | } 47 | } 48 | }` 49 | 50 | packageLockJson := path.Join(tmpDir, "package-lock.json") 51 | 52 | if err := os.WriteFile(packageLockJson, []byte(packageLock), 0644); err != nil { 53 | t.Fatal(err) 54 | } 55 | 56 | assert.NoError(t, patchPackageLockToRemoveCanIUsePackage(packageLockJson)) 57 | 58 | updatedPackageLock, err := os.ReadFile(packageLockJson) 59 | 60 | assert.NoError(t, err) 61 | 62 | assert.NotContains(t, string(updatedPackageLock), "node_modules/caniuse-lite") 63 | } 64 | -------------------------------------------------------------------------------- /extension/changelog_test.go: -------------------------------------------------------------------------------- 1 | package extension 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestChangelogParsing(t *testing.T) { 10 | content, err := parseMarkdownChangelog("# 1.0.0\n\n- Test\n- Test2\n# 2.0.0\n- Test3\n- Test4\n") 11 | assert.NoError(t, err) 12 | 13 | assert.Equal(t, "\n", content["1.0.0"]) 14 | assert.Equal(t, "\n", content["2.0.0"]) 15 | } 16 | 17 | func TestChangelogParsingWhitespaces(t *testing.T) { 18 | content, err := parseMarkdownChangelog("# 1.0.0\n \n- Test\n- Test2\n# 2.0.0\n- Test3\n - Test4\n") 19 | assert.NoError(t, err) 20 | 21 | assert.Equal(t, "\n", content["1.0.0"]) 22 | assert.Equal(t, "\n", content["2.0.0"]) 23 | } 24 | -------------------------------------------------------------------------------- /extension/composer-info.zip.zst: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shopware/shopware-cli/c353be3773605de540ace8684d24c7530fc97131/extension/composer-info.zip.zst -------------------------------------------------------------------------------- /extension/composer_info.go: -------------------------------------------------------------------------------- 1 | package extension 2 | 3 | import ( 4 | "archive/zip" 5 | "bytes" 6 | _ "embed" 7 | "io" 8 | 9 | "github.com/klauspost/compress/zstd" 10 | ) 11 | 12 | //go:embed composer-info.zip.zst 13 | var composerInfoFile []byte 14 | 15 | func getComposerInfoFS() (*zip.Reader, error) { 16 | zstReader, err := zstd.NewReader(bytes.NewReader(composerInfoFile)) 17 | if err != nil { 18 | return nil, err 19 | } 20 | 21 | defer zstReader.Close() 22 | 23 | uncompressed, err := io.ReadAll(zstReader) 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | return zip.NewReader(bytes.NewReader(uncompressed), int64(len(uncompressed))) 29 | } 30 | -------------------------------------------------------------------------------- /extension/config_test.go: -------------------------------------------------------------------------------- 1 | package extension 2 | 3 | import ( 4 | "os" 5 | "path" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestConfigValidationStringListDecode(t *testing.T) { 12 | cfg := ` 13 | validation: 14 | ignore: 15 | - metadata.setup 16 | - metadata.setup.path 17 | ` 18 | 19 | tmpDir := t.TempDir() 20 | 21 | assert.NoError(t, os.WriteFile(path.Join(tmpDir, ".shopware-extension.yaml"), []byte(cfg), 0o644)) 22 | 23 | ext, err := readExtensionConfig(tmpDir) 24 | assert.NoError(t, err) 25 | assert.Equal(t, 2, len(ext.Validation.Ignore)) 26 | assert.Equal(t, "metadata.setup", ext.Validation.Ignore[0].Identifier) 27 | assert.Equal(t, "metadata.setup.path", ext.Validation.Ignore[1].Identifier) 28 | } 29 | 30 | func TestConfigValidationStringObjectDecode(t *testing.T) { 31 | cfg := ` 32 | validation: 33 | ignore: 34 | - identifier: metadata.setup 35 | - identifier: foo 36 | path: bar 37 | ` 38 | 39 | tmpDir := t.TempDir() 40 | 41 | assert.NoError(t, os.WriteFile(path.Join(tmpDir, ".shopware-extension.yaml"), []byte(cfg), 0o644)) 42 | 43 | ext, err := readExtensionConfig(tmpDir) 44 | assert.NoError(t, err) 45 | assert.Equal(t, 2, len(ext.Validation.Ignore)) 46 | assert.Equal(t, "metadata.setup", ext.Validation.Ignore[0].Identifier) 47 | assert.Equal(t, "foo", ext.Validation.Ignore[1].Identifier) 48 | assert.Equal(t, "bar", ext.Validation.Ignore[1].Path) 49 | } 50 | -------------------------------------------------------------------------------- /extension/git.go: -------------------------------------------------------------------------------- 1 | package extension 2 | 3 | import ( 4 | "archive/zip" 5 | "bytes" 6 | "fmt" 7 | "os/exec" 8 | "strings" 9 | ) 10 | 11 | func gitTagOrBranchOfFolder(source string) (string, error) { 12 | tagCmd := exec.Command("git", "-C", source, "tag", "--sort=-creatordate") 13 | 14 | stdout, err := tagCmd.Output() 15 | if err != nil { 16 | return "", err 17 | } 18 | 19 | versions := strings.Split(string(stdout), "\n") 20 | 21 | if len(versions) > 0 && len(versions[0]) > 0 { 22 | return versions[0], nil 23 | } 24 | 25 | branchCmd := exec.Command("git", "-C", source, "rev-parse", "--abbrev-ref", "HEAD") 26 | 27 | stdout, err = branchCmd.Output() 28 | 29 | if err != nil { 30 | return "", fmt.Errorf("gitTagOrBranchOfFolder: %v", err) 31 | } 32 | 33 | return strings.Trim(strings.TrimLeft(string(stdout), "* "), "\n"), nil 34 | } 35 | 36 | func GitCopyFolder(source, target, commitHash string) (string, error) { 37 | var err error 38 | if commitHash == "" { 39 | commitHash, err = gitTagOrBranchOfFolder(source) 40 | 41 | if err != nil { 42 | return "", fmt.Errorf("GitCopyFolder: cannot find checkout tag or branch: %v", err) 43 | } 44 | } 45 | 46 | archiveCmd := exec.Command("git", "-C", source, "archive", commitHash, "--format=zip") 47 | 48 | stdout, err := archiveCmd.Output() 49 | if err != nil { 50 | return "", fmt.Errorf("GitCopyFolder: cannot archive %s: %v", commitHash, err) 51 | } 52 | 53 | zipReader, err := zip.NewReader(bytes.NewReader(stdout), int64(len(stdout))) 54 | if err != nil { 55 | return "", fmt.Errorf("GitCopyFolder: cannot open the zip file produced by git archive: %v", err) 56 | } 57 | 58 | err = Unzip(zipReader, target) 59 | if err != nil { 60 | return "", fmt.Errorf("GitCopyFolder: cannot unzip the zip archive: %v", err) 61 | } 62 | 63 | return commitHash, err 64 | } 65 | -------------------------------------------------------------------------------- /extension/manifest_test.go: -------------------------------------------------------------------------------- 1 | package extension 2 | 3 | import ( 4 | "encoding/xml" 5 | "os" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestManifestRead(t *testing.T) { 12 | bytes, err := os.ReadFile("_fixtures/istorier.xml") 13 | 14 | assert.NoError(t, err) 15 | 16 | manifest := Manifest{} 17 | 18 | assert.NoError(t, xml.Unmarshal(bytes, &manifest)) 19 | 20 | assert.Equal(t, "InstoImmersiveElements", manifest.Meta.Name) 21 | assert.Equal(t, "Immersive Elements", manifest.Meta.Label[0].Value) 22 | assert.Equal(t, "Transform your online store into an unforgettable brand experience. As an incredibly cost-effective alternative to external resources, the app is engineered to boost conversions.", manifest.Meta.Description[0].Value) 23 | assert.Equal(t, "Instorier AS", manifest.Meta.Author) 24 | assert.Equal(t, "(c) by Instorier AS", manifest.Meta.Copyright) 25 | assert.Equal(t, "1.1.0", manifest.Meta.Version) 26 | assert.Equal(t, "Resources/config/plugin.png", manifest.Meta.Icon) 27 | assert.Equal(t, "Proprietary", manifest.Meta.License) 28 | 29 | assert.Equal(t, "https://instorier.apps.shopware.io/app/lifecycle/register", manifest.Setup.RegistrationUrl) 30 | assert.Equal(t, "", manifest.Setup.Secret) 31 | 32 | assert.Equal(t, "https://instorier.apps.shopware.io/iframe", manifest.Admin.BaseAppUrl) 33 | 34 | assert.Len(t, manifest.Permissions.Read, 57) 35 | assert.Len(t, manifest.Permissions.Create, 4) 36 | assert.Len(t, manifest.Permissions.Update, 2) 37 | assert.Len(t, manifest.Permissions.Delete, 2) 38 | } 39 | -------------------------------------------------------------------------------- /extension/theme.go: -------------------------------------------------------------------------------- 1 | package extension 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | ) 8 | 9 | func validateTheme(ctx *ValidationContext) { 10 | themeJSONPath := fmt.Sprintf("%s/src/Resources/theme.json", ctx.Extension.GetPath()) 11 | 12 | if _, err := os.Stat(themeJSONPath); !os.IsNotExist(err) { 13 | content, err := os.ReadFile(themeJSONPath) 14 | if err != nil { 15 | ctx.AddError("theme.validator", "Invalid theme.json") 16 | return 17 | } 18 | 19 | var theme themeJSON 20 | err = json.Unmarshal(content, &theme) 21 | if err != nil { 22 | ctx.AddError("theme.validator", "Cannot decode theme.json") 23 | return 24 | } 25 | 26 | if len(theme.PreviewMedia) == 0 { 27 | ctx.AddError("theme.validator", "Required field \"previewMedia\" missing in theme.json") 28 | return 29 | } 30 | 31 | expectedMediaPath := fmt.Sprintf("%s/src/Resources/%s", ctx.Extension.GetPath(), theme.PreviewMedia) 32 | 33 | if _, err := os.Stat(expectedMediaPath); os.IsNotExist(err) { 34 | ctx.AddError("theme.validator", fmt.Sprintf("Theme preview image file is expected to be placed at %s, but not found there.", expectedMediaPath)) 35 | } 36 | } 37 | } 38 | 39 | type themeJSON struct { 40 | PreviewMedia string `json:"previewMedia"` 41 | } 42 | -------------------------------------------------------------------------------- /extension/util.go: -------------------------------------------------------------------------------- 1 | package extension 2 | 3 | import ( 4 | "os" 5 | "path" 6 | "path/filepath" 7 | "strings" 8 | 9 | "github.com/joho/godotenv" 10 | ) 11 | 12 | func PlatformPath(projectRoot, component, path string) string { 13 | if _, err := os.Stat(filepath.Join(projectRoot, "src", "Core", "composer.json")); err == nil { 14 | return filepath.Join(projectRoot, "src", component, path) 15 | } else if _, err := os.Stat(filepath.Join(projectRoot, "vendor", "shopware", "platform")); err == nil { 16 | return filepath.Join(projectRoot, "vendor", "shopware", "platform", "src", component, path) 17 | } 18 | 19 | return filepath.Join(projectRoot, "vendor", "shopware", strings.ToLower(component), path) 20 | } 21 | 22 | // IsContributeProject checks if the project is a contribution project aka shopware/shopware. 23 | func IsContributeProject(projectRoot string) bool { 24 | if _, err := os.Stat(filepath.Join(projectRoot, "src", "Core", "composer.json")); err == nil { 25 | return true 26 | } 27 | 28 | return false 29 | } 30 | 31 | // LoadSymfonyEnvFile loads the Symfony .env file from the project root. 32 | func LoadSymfonyEnvFile(projectRoot string) error { 33 | currentEnv := os.Getenv("APP_ENV") 34 | if currentEnv == "" { 35 | currentEnv = "dev" 36 | } 37 | 38 | possibleEnvFiles := []string{ 39 | path.Join(projectRoot, ".env.dist"), 40 | path.Join(projectRoot, ".env"), 41 | path.Join(projectRoot, ".env.local"), 42 | path.Join(projectRoot, ".env."+currentEnv), 43 | path.Join(projectRoot, ".env."+currentEnv+".local"), 44 | } 45 | 46 | var foundEnvFiles []string 47 | 48 | for _, envFile := range possibleEnvFiles { 49 | if _, err := os.Stat(envFile); err == nil { 50 | foundEnvFiles = append(foundEnvFiles, envFile) 51 | } 52 | } 53 | 54 | if len(foundEnvFiles) == 0 { 55 | return nil 56 | } 57 | 58 | currentMap, err := godotenv.Read(foundEnvFiles...) 59 | if err != nil { 60 | return err 61 | } 62 | 63 | for key, value := range currentMap { 64 | if os.Getenv(key) == "" { 65 | if err := os.Setenv(key, value); err != nil { 66 | return err 67 | } 68 | } 69 | } 70 | 71 | return nil 72 | } 73 | -------------------------------------------------------------------------------- /internal/account-api/updates.go: -------------------------------------------------------------------------------- 1 | package account_api 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "fmt" 8 | "io" 9 | "net/http" 10 | 11 | "github.com/shopware/shopware-cli/logging" 12 | ) 13 | 14 | type UpdateCheckExtension struct { 15 | Name string `json:"name"` 16 | Version string `json:"version"` 17 | } 18 | 19 | type UpdateCheckExtensionCompatibility struct { 20 | Name string `json:"name"` 21 | Label string `json:"label"` 22 | IconPath string `json:"iconPath"` 23 | Status struct { 24 | Name string `json:"name"` 25 | Label string `json:"label"` 26 | Type string `json:"type"` 27 | } `json:"status"` 28 | } 29 | 30 | func GetFutureExtensionUpdates(ctx context.Context, currentVersion string, futureVersion string, extensions []UpdateCheckExtension) ([]UpdateCheckExtensionCompatibility, error) { 31 | req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://api.shopware.com/swplatform/autoupdate", nil) 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | q := req.URL.Query() 37 | q.Set("language", "en-GB") 38 | q.Set("shopwareVersion", currentVersion) 39 | req.URL.RawQuery = q.Encode() 40 | 41 | bodyBytes, err := json.Marshal(map[string]interface{}{ 42 | "futureShopwareVersion": futureVersion, 43 | "plugins": extensions, 44 | }) 45 | if err != nil { 46 | return nil, err 47 | } 48 | req.Body = io.NopCloser(bytes.NewReader(bodyBytes)) 49 | req.Header.Set("Content-Type", "application/json") 50 | 51 | resp, err := http.DefaultClient.Do(req) 52 | if err != nil { 53 | return nil, err 54 | } 55 | defer func() { 56 | if err := resp.Body.Close(); err != nil { 57 | logging.FromContext(ctx).Errorf("Cannot close response body: %v", err) 58 | } 59 | }() 60 | 61 | if resp.StatusCode != http.StatusOK { 62 | body, _ := io.ReadAll(resp.Body) 63 | return nil, fmt.Errorf("API returned non-OK status: %d\n%s", resp.StatusCode, string(body)) 64 | } 65 | 66 | var compatibilityResults []UpdateCheckExtensionCompatibility 67 | if err := json.NewDecoder(resp.Body).Decode(&compatibilityResults); err != nil { 68 | return nil, err 69 | } 70 | 71 | return compatibilityResults, nil 72 | } 73 | -------------------------------------------------------------------------------- /internal/asset/source.go: -------------------------------------------------------------------------------- 1 | package asset 2 | 3 | type Source struct { 4 | Name string 5 | Path string 6 | AdminEsbuildCompatible bool 7 | StorefrontEsbuildCompatible bool 8 | DisableSass bool 9 | NpmStrict bool 10 | } 11 | -------------------------------------------------------------------------------- /internal/changelog/changelog.tpl: -------------------------------------------------------------------------------- 1 | {{range .Commits}}- [{{ .Message }}]({{ $.Config.VCSURL }}/{{ .Hash }}) 2 | {{end}} -------------------------------------------------------------------------------- /internal/changelog/changelog_test.go: -------------------------------------------------------------------------------- 1 | package changelog 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | 8 | "github.com/shopware/shopware-cli/internal/git" 9 | ) 10 | 11 | func TestGenerateWithoutConfig(t *testing.T) { 12 | commits := []git.GitCommit{ 13 | { 14 | Message: "feat: add new feature", 15 | Hash: "1234567890", 16 | }, 17 | } 18 | 19 | changelog, err := renderChangelog(commits, Config{ 20 | VCSURL: "https://github.com/FriendsOfShopware/FroshTools/commit", 21 | Template: defaultChangelogTpl, 22 | }) 23 | 24 | assert.NoError(t, err) 25 | 26 | assert.Equal(t, "- [feat: add new feature](https://github.com/FriendsOfShopware/FroshTools/commit/1234567890)", changelog) 27 | } 28 | 29 | func TestTicketParsing(t *testing.T) { 30 | commits := []git.GitCommit{ 31 | { 32 | Message: "NEXT-1234 - Fooo", 33 | Hash: "1234567890", 34 | }, 35 | } 36 | 37 | cfg := Config{ 38 | Variables: map[string]string{ 39 | "ticket": "^(NEXT-[0-9]+)", 40 | }, 41 | Template: "{{range .Commits}}- [{{ .Message }}](https://issues.shopware.com/issues/{{ .Variables.ticket }}){{end}}", 42 | } 43 | 44 | changelog, err := renderChangelog(commits, cfg) 45 | 46 | assert.NoError(t, err) 47 | assert.Equal(t, "- [NEXT-1234 - Fooo](https://issues.shopware.com/issues/NEXT-1234)", changelog) 48 | } 49 | 50 | func TestIncludeFilters(t *testing.T) { 51 | commits := []git.GitCommit{ 52 | { 53 | Message: "NEXT-1234 - Fooo", 54 | Hash: "1234567890", 55 | }, 56 | { 57 | Message: "merge foo", 58 | Hash: "1234567890", 59 | }, 60 | } 61 | 62 | cfg := Config{ 63 | Pattern: "^(NEXT-[0-9]+)", 64 | Template: defaultChangelogTpl, 65 | } 66 | 67 | changelog, err := renderChangelog(commits, cfg) 68 | 69 | assert.NoError(t, err) 70 | assert.Equal(t, "- [NEXT-1234 - Fooo](/1234567890)", changelog) 71 | } 72 | -------------------------------------------------------------------------------- /internal/color/color.go: -------------------------------------------------------------------------------- 1 | package color 2 | 3 | import "github.com/charmbracelet/lipgloss" 4 | 5 | var GreenText = lipgloss.NewStyle().Foreground(lipgloss.Color("#04B575")) 6 | -------------------------------------------------------------------------------- /internal/config/testdata/.shopware-cli.yml: -------------------------------------------------------------------------------- 1 | account: 2 | email: test@test.com 3 | password: test123 4 | company: 456 -------------------------------------------------------------------------------- /internal/config/testdata/write-test.yml: -------------------------------------------------------------------------------- 1 | account: 2 | email: test@test.com 3 | password: test123 4 | company: 456 5 | -------------------------------------------------------------------------------- /internal/esbuild/download_windows.go: -------------------------------------------------------------------------------- 1 | package esbuild 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | "os" 10 | "path/filepath" 11 | "strings" 12 | 13 | "github.com/klauspost/compress/zip" 14 | ) 15 | 16 | func downloadDartSass(ctx context.Context, cacheDir string) error { 17 | request, _ := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("https://github.com/sass/dart-sass/releases/download/%s/dart-sass-%s-windows-x64.zip", dartSassVersion, dartSassVersion), nil) 18 | 19 | zipFile, err := http.DefaultClient.Do(request) 20 | 21 | if err != nil { 22 | return fmt.Errorf("cannot download dart-sass: %w", err) 23 | } 24 | 25 | defer zipFile.Body.Close() 26 | 27 | if zipFile.StatusCode != 200 { 28 | return fmt.Errorf("cannot download dart-sass: %s with http code %s", zipFile.Request.URL, zipFile.Status) 29 | } 30 | 31 | zipBytes, err := io.ReadAll(zipFile.Body) 32 | 33 | if err != nil { 34 | return fmt.Errorf("cannot read zip file: %w", err) 35 | } 36 | 37 | zipReader, err := zip.NewReader(bytes.NewReader(zipBytes), int64(len(zipBytes))) 38 | 39 | if err != nil { 40 | return fmt.Errorf("cannot read zip file: %w", err) 41 | } 42 | 43 | for _, zipFile := range zipReader.File { 44 | name := strings.TrimPrefix(zipFile.Name, "dart-sass/") 45 | 46 | if strings.Contains(name, "..") { 47 | continue 48 | } 49 | 50 | zipHandle, err := zipFile.Open() 51 | 52 | if err != nil { 53 | return fmt.Errorf("cannot open zip file: %w", err) 54 | } 55 | 56 | extractedPath := filepath.Join(cacheDir, name) 57 | extractedFolder := filepath.Dir(extractedPath) 58 | 59 | if _, err := os.Stat(extractedFolder); os.IsNotExist(err) { 60 | if err := os.MkdirAll(extractedFolder, os.ModePerm); err != nil { 61 | return fmt.Errorf("cannot create dart-sass in temp: %w", err) 62 | } 63 | } 64 | 65 | outFile, err := os.Create(extractedPath) 66 | 67 | if err != nil { 68 | return fmt.Errorf("cannot create dart-sass in temp: %w", err) 69 | } 70 | 71 | if _, err := io.CopyN(outFile, zipHandle, int64(zipFile.UncompressedSize64)); err != nil { 72 | return fmt.Errorf("cannot copy dart-sass in temp: %w", err) 73 | } 74 | 75 | if err := outFile.Close(); err != nil { 76 | return fmt.Errorf("cannot close dart-sass in temp: %w", err) 77 | } 78 | } 79 | 80 | return nil 81 | } 82 | -------------------------------------------------------------------------------- /internal/esbuild/sass.go: -------------------------------------------------------------------------------- 1 | package esbuild 2 | 3 | import ( 4 | "context" 5 | _ "embed" 6 | "os" 7 | "os/exec" 8 | "path" 9 | "path/filepath" 10 | "runtime" 11 | 12 | "github.com/shopware/shopware-cli/internal/system" 13 | "github.com/shopware/shopware-cli/logging" 14 | ) 15 | 16 | const dartSassVersion = "1.69.7" 17 | 18 | //go:embed static/variables.scss 19 | var scssVariables []byte 20 | 21 | //go:embed static/mixins.scss 22 | var scssMixins []byte 23 | 24 | func locateDartSass(ctx context.Context) (string, error) { 25 | if exePath, err := exec.LookPath("dart-sass"); err == nil { 26 | return exePath, nil 27 | } 28 | 29 | cacheDir := path.Join(system.GetShopwareCliCacheDir(), "dart-sass", dartSassVersion) 30 | 31 | expectedPath := path.Join(cacheDir, "sass") 32 | 33 | //goland:noinspection ALL 34 | if runtime.GOOS == "windows" { 35 | expectedPath += ".bat" 36 | } 37 | 38 | if _, err := os.Stat(expectedPath); err == nil { 39 | return expectedPath, nil 40 | } 41 | 42 | if _, err := os.Stat(filepath.Dir(expectedPath)); os.IsNotExist(err) { 43 | if err := os.MkdirAll(filepath.Dir(expectedPath), os.ModePerm); err != nil { 44 | return "", err 45 | } 46 | } 47 | 48 | logging.FromContext(ctx).Infof("Downloading dart-sass") 49 | 50 | if err := downloadDartSass(ctx, cacheDir); err != nil { 51 | return "", err 52 | } 53 | 54 | return expectedPath, nil 55 | } 56 | -------------------------------------------------------------------------------- /internal/esbuild/static/mixins.scss: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------- 2 | // This file contains all application-wide Sass mixins. 3 | // ----------------------------------------------------------------------------- 4 | 5 | /// @type Display Model 6 | @mixin flex-centering() { 7 | display: flex; 8 | align-items: center; 9 | justify-content: center; 10 | } 11 | 12 | @mixin flex-centering-vertical() { 13 | display: flex; 14 | align-items: center; 15 | } 16 | 17 | @mixin flex-centering-horizontal() { 18 | display: flex; 19 | justify-content: center; 20 | } 21 | 22 | /// @type Box 23 | @mixin square($dimension) { 24 | width: $dimension; 25 | height: $dimension; 26 | } 27 | 28 | @mixin circle($dimension) { 29 | width: $dimension; 30 | height: $dimension; 31 | border-radius: 50%; 32 | } 33 | 34 | @mixin size($size) { 35 | width: $size; 36 | height: $size; 37 | } 38 | 39 | /// @type Text 40 | @mixin truncate() { 41 | white-space: nowrap; 42 | text-overflow: ellipsis; 43 | overflow: hidden; 44 | } 45 | 46 | @mixin reset-truncate() { 47 | white-space: inherit; 48 | text-overflow: inherit; 49 | overflow: visible; 50 | } 51 | 52 | @mixin animated-truncate() { 53 | position: relative; 54 | margin-right: 18px; 55 | 56 | &::after { 57 | position: absolute; 58 | content: "\2026"; 59 | display: inline-block; 60 | overflow: hidden; 61 | width: 0; 62 | margin-left: 3px; 63 | animation: ellipsis steps(4, end) 1.5s infinite; 64 | vertical-align: bottom; 65 | top: 0; 66 | } 67 | } 68 | 69 | @keyframes ellipsis { 70 | to { 71 | width: 1.25rem; 72 | } 73 | } 74 | 75 | /// @type Other 76 | @mixin transition($what: all, $time: 0.3s, $how: ease) { 77 | transition: $what $time $how; 78 | } 79 | 80 | @mixin hidden() { 81 | position: absolute; 82 | top: -9999px; 83 | left: -9999px; 84 | } 85 | 86 | @mixin drop-shadow-default() { 87 | box-shadow: 0 1px 1px rgba(0, 0, 0, 8%), 0 2px 1px rgba(0, 0, 0, 6%), 0 1px 3px rgba(0, 0, 0, 10%); 88 | } -------------------------------------------------------------------------------- /internal/esbuild/util.go: -------------------------------------------------------------------------------- 1 | package esbuild 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | ) 7 | 8 | var matchLetter = regexp.MustCompile(`[A-Z]`) 9 | 10 | // @see https://github.com/symfony/symfony/blob/6.3/src/Symfony/Component/Serializer/NameConverter/CamelCaseToSnakeCaseNameConverter.php#L31 11 | func ToKebabCase(str string) string { 12 | converted := matchLetter.ReplaceAllStringFunc(str, func(match string) string { 13 | return "-" + strings.ToLower(match) 14 | }) 15 | 16 | // See https://github.com/shopware/shopware/blob/240386d/src/Core/Framework/Plugin/BundleConfigGenerator.php#L73 17 | converted = strings.ReplaceAll(converted, "_", "-") 18 | 19 | return strings.TrimPrefix(converted, "-") 20 | } 21 | 22 | // @see https://github.com/symfony/symfony/blob/7.2/src/Symfony/Bundle/FrameworkBundle/Command/AssetsInstallCommand.php#L128 23 | func toBundleFolderName(name string) string { 24 | assetDir := strings.ToLower(name) 25 | assetDir = strings.TrimSuffix(assetDir, "bundle") 26 | return assetDir 27 | } 28 | -------------------------------------------------------------------------------- /internal/esbuild/util_test.go: -------------------------------------------------------------------------------- 1 | package esbuild 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestKebabCase(t *testing.T) { 10 | assert.Equal(t, "foo-bar", ToKebabCase("FooBar")) 11 | assert.Equal(t, "f-o-o-bar-baz", ToKebabCase("FOOBarBaz")) 12 | assert.Equal(t, "frosh-tools", ToKebabCase("FroshTools")) 13 | assert.Equal(t, "my-module-name-s-w6", ToKebabCase("MyModuleNameSW6")) 14 | assert.Equal(t, "a-i-search", ToKebabCase("AISearch")) 15 | assert.Equal(t, "mediameets-fb-pixel", ToKebabCase("mediameetsFbPixel")) 16 | assert.Equal(t, "wwbla-bar-foo", ToKebabCase("wwblaBarFoo")) 17 | assert.Equal(t, "with-underscore", ToKebabCase("with_underscore")) 18 | } 19 | 20 | func TestBundleFolderName(t *testing.T) { 21 | assert.Equal(t, "myplugin", toBundleFolderName("MyPluginBundle")) 22 | assert.Equal(t, "anotherplugin", toBundleFolderName("AnotherPluginBundle")) 23 | assert.Equal(t, "simpleplugin", toBundleFolderName("SimplePlugin")) 24 | assert.Equal(t, "plugin", toBundleFolderName("PluginBundle")) 25 | assert.Equal(t, "plugin", toBundleFolderName("Plugin")) 26 | } 27 | -------------------------------------------------------------------------------- /internal/flexmigrator/env.go: -------------------------------------------------------------------------------- 1 | package flexmigrator 2 | 3 | import ( 4 | "os" 5 | "path" 6 | ) 7 | 8 | func MigrateEnv(project string) error { 9 | _, envLocalErr := os.Stat(path.Join(project, ".env.local")) 10 | _, envErr := os.Stat(path.Join(project, ".env")) 11 | 12 | if os.IsNotExist(envLocalErr) && !os.IsNotExist(envErr) { 13 | if err := os.Rename(path.Join(project, ".env"), path.Join(project, ".env.local")); err != nil { 14 | return err 15 | } 16 | 17 | return os.WriteFile(path.Join(project, ".env"), []byte(""), os.ModePerm) 18 | } 19 | 20 | return nil 21 | } 22 | -------------------------------------------------------------------------------- /internal/html/testdata/01-basic-element.txt: -------------------------------------------------------------------------------- 1 | Click me 2 | ----- 3 | Click me -------------------------------------------------------------------------------- /internal/html/testdata/02-sub-nodes.txt: -------------------------------------------------------------------------------- 1 | 2 | ----- 3 | -------------------------------------------------------------------------------- /internal/html/testdata/03-attributes-single.txt: -------------------------------------------------------------------------------- 1 | Click me 2 | ----- 3 | Click me -------------------------------------------------------------------------------- /internal/html/testdata/04-attributes.txt: -------------------------------------------------------------------------------- 1 | Click me 2 | ----- 3 | Click me -------------------------------------------------------------------------------- /internal/html/testdata/05-children-with-comment.txt: -------------------------------------------------------------------------------- 1 | 2 | ----- 3 | -------------------------------------------------------------------------------- /internal/html/testdata/06-multiple-comments.txt: -------------------------------------------------------------------------------- 1 |
Content
2 | ----- 3 |
Content
-------------------------------------------------------------------------------- /internal/html/testdata/07-comment-with-nested-tags.txt: -------------------------------------------------------------------------------- 1 |
actual content
2 | ----- 3 | 4 |
actual content
-------------------------------------------------------------------------------- /internal/html/testdata/08-comment-with-special-characters.txt: -------------------------------------------------------------------------------- 1 |
2 | ----- 3 |
-------------------------------------------------------------------------------- /internal/html/testdata/09-elements-with-block.txt: -------------------------------------------------------------------------------- 1 | {% block foo %}Click me{% endblock %} 2 | ----- 3 | {% block foo %} 4 | Click me 5 | {% endblock %} -------------------------------------------------------------------------------- /internal/html/testdata/10-multi-line-breaks-get-removed.txt: -------------------------------------------------------------------------------- 1 | {% block test %}Click me 2 | 3 | 4 | Click me{% endblock %} 5 | ----- 6 | {% block test %} 7 | Click me 8 | 9 | Click me 10 | {% endblock %} -------------------------------------------------------------------------------- /internal/html/testdata/11-multi-line-between-elements-only-one.txt: -------------------------------------------------------------------------------- 1 | 2 | ----- 3 | -------------------------------------------------------------------------------- /internal/html/testdata/12-multi-line-between-only-elements.txt: -------------------------------------------------------------------------------- 1 | 10 | ----- 11 | -------------------------------------------------------------------------------- /internal/html/testdata/13-long-attribute-is-on-new-line.txt: -------------------------------------------------------------------------------- 1 | 2 | ----- 3 | -------------------------------------------------------------------------------- /internal/html/testdata/14-html-element-with-content.txt: -------------------------------------------------------------------------------- 1 | 2 | ----- 3 | -------------------------------------------------------------------------------- /internal/html/testdata/15-multiple-template-elements.txt: -------------------------------------------------------------------------------- 1 | 2 | ----- 3 | 6 | 7 | -------------------------------------------------------------------------------- /internal/html/testdata/16-multiple-template-elements-with-root.txt: -------------------------------------------------------------------------------- 1 | 2 | ----- 3 | 4 | 7 | 8 | 11 | -------------------------------------------------------------------------------- /internal/html/testdata/17-starting-tag-in-html-node.txt: -------------------------------------------------------------------------------- 1 |

{{ $tc('swag-customized-products.detail.tabGeneral.cardExclusion.emptyTitle', (searchTerm.length <= 0) ? 1 : 0) }}

2 | ----- 3 |

4 | {{ $tc('swag-customized-products.detail.tabGeneral.cardExclusion.emptyTitle', (searchTerm.length <= 0) ? 1 : 0) }} 5 |

-------------------------------------------------------------------------------- /internal/html/testdata/18-template-expression-in-div.txt: -------------------------------------------------------------------------------- 1 |
{{ someVariable }}
2 | ----- 3 |
{{ someVariable }}
-------------------------------------------------------------------------------- /internal/html/testdata/19-multiple-template-expressions.txt: -------------------------------------------------------------------------------- 1 |
{{ firstVar }}{{ secondVar }}
2 | ----- 3 |
{{ firstVar }}{{ secondVar }}
-------------------------------------------------------------------------------- /internal/html/testdata/20-template-expression-with-text.txt: -------------------------------------------------------------------------------- 1 |
Before {{ expression }} After
2 | ----- 3 |
Before {{ expression }} After
-------------------------------------------------------------------------------- /internal/html/testdata/21-template-expression-in-nested-elements.txt: -------------------------------------------------------------------------------- 1 |
{{ nestedExpression }}
2 | ----- 3 |
4 | {{ nestedExpression }} 5 |
-------------------------------------------------------------------------------- /internal/html/testdata/22-template-expression-in-router-link.txt: -------------------------------------------------------------------------------- 1 | {{ item.mainPseudovariant.product.translated.name }} 2 | ----- 3 | 4 | {{ item.mainPseudovariant.product.translated.name }} 5 | -------------------------------------------------------------------------------- /internal/html/testdata/23-multiple-long-template-expressions.txt: -------------------------------------------------------------------------------- 1 |
{{ item.mainPseudovariant.product.translated.name }}{{ item.mainPseudovariant.product.translated.description }}
2 | ----- 3 |
4 | {{ item.mainPseudovariant.product.translated.name }} 5 | {{ item.mainPseudovariant.product.translated.description }} 6 |
-------------------------------------------------------------------------------- /internal/html/testdata/24-html-comment-before-element.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 |

Content

4 |
5 | ----- 6 | 7 | 8 |

Content

9 |
-------------------------------------------------------------------------------- /internal/html/testdata/25-if.txt: -------------------------------------------------------------------------------- 1 | {% if foo %} 2 | 3 | {% endif %} 4 | ----- 5 | {% if foo %} 6 | 7 | {% endif %} -------------------------------------------------------------------------------- /internal/html/testdata/26-if-else.txt: -------------------------------------------------------------------------------- 1 | {% if foo %} 2 | 3 | {% else %} 4 | 5 | {% endif %} 6 | ----- 7 | {% if foo %} 8 | 9 | {% else %} 10 | 11 | {% endif %} -------------------------------------------------------------------------------- /internal/html/testdata/27-if-elseif-else.txt: -------------------------------------------------------------------------------- 1 | {% if foo %} 2 | 3 | {% elseif bla %} 4 | 5 | {% elseif yea %} 6 | 7 | {% else %} 8 | 9 | {% endif %} 10 | ----- 11 | {% if foo %} 12 | 13 | {% elseif bla %} 14 | 15 | {% elseif yea %} 16 | 17 | {% else %} 18 | 19 | {% endif %} -------------------------------------------------------------------------------- /internal/html/testdata/28-if-while-attrs.txt: -------------------------------------------------------------------------------- 1 | 2 | ----- 3 | -------------------------------------------------------------------------------- /internal/html/testdata/29-block-nesting.txt: -------------------------------------------------------------------------------- 1 | {% block a %} 2 | {% block b %} 3 | {% block c %} 4 | {% block d %} 5 | {% endblock %} 6 | {% endblock %} 7 | {% endblock %} 8 | {% endblock %} 9 | ----- 10 | {% block a %} 11 | {% block b %} 12 | {% block c %} 13 | {% block d %}{% endblock %} 14 | {% endblock %} 15 | {% endblock %} 16 | {% endblock %} -------------------------------------------------------------------------------- /internal/html/testdata/30-attribute-long-closing-correctly-formatted.txt: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Hello World

4 |
5 |
6 | ----- 7 |
8 |
11 |

Hello World

12 |
13 |
-------------------------------------------------------------------------------- /internal/html/testdata/31-block-parent.txt: -------------------------------------------------------------------------------- 1 | {% block a %} 2 | {% block b %} 3 | {% parent() %} 4 | {% endblock %} 5 | {% endblock %} 6 | ----- 7 | {% block a %} 8 | {% block b %} 9 | {% parent() %} 10 | {% endblock %} 11 | {% endblock %} -------------------------------------------------------------------------------- /internal/html/testdata/32-multi-attribute-selfclose.txt: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | ----- 5 |
6 | 10 |
-------------------------------------------------------------------------------- /internal/html/testdata/33-comment-over-block.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {% block sw_import_export_tabs_profiles %}{% endblock %} 6 | ----- 7 | 8 | {% block sw_import_export_tabs_profiles %}{% endblock %} -------------------------------------------------------------------------------- /internal/html/testdata/34-comment-over-block-nested.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {% block sw_import_export_tabs_profiles %} 6 | 7 | 8 | 9 | {% block foo %} 10 | {% endblock %} 11 | {% endblock %} 12 | ----- 13 | 14 | {% block sw_import_export_tabs_profiles %} 15 | 16 | {% block foo %}{% endblock %} 17 | {% endblock %} -------------------------------------------------------------------------------- /internal/html/testdata/35-element-content-format.txt: -------------------------------------------------------------------------------- 1 | {{ $tc('iwvs-import-export.page.colorTab') }} 2 | ----- 3 | 4 | {{ $tc('iwvs-import-export.page.colorTab') }} 5 | -------------------------------------------------------------------------------- /internal/html/testdata/36-block-around-if.txt: -------------------------------------------------------------------------------- 1 | 2 | {% block sw_cms_element_product_slider_config_settings_min_width %} 3 | {% parent %} 4 | 5 | 6 | {% block sw_cms_element_product_slider_config_settings_slides_mobile %} 7 | 19 | {% endblock %} 20 | {% endblock %} 21 | ----- 22 | 23 | {% block sw_cms_element_product_slider_config_settings_min_width %} 24 | {% parent() %} 25 | 26 | 27 | {% block sw_cms_element_product_slider_config_settings_slides_mobile %} 28 | 40 | {% endblock %} 41 | {% endblock %} -------------------------------------------------------------------------------- /internal/html/testdata/37-formatting-element.txt: -------------------------------------------------------------------------------- 1 | {% block sw_cms_block_product_listing_preview %} 2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | 10 | 11 |
12 | {% endblock %} 13 | ----- 14 | {% block sw_cms_block_product_listing_preview %} 15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | 23 | 24 |
25 | {% endblock %} -------------------------------------------------------------------------------- /internal/html/testdata/38-formatting-element-content-oneliner.txt: -------------------------------------------------------------------------------- 1 |
2 | {{ somewhatLongFooBarBaz }}/{{ somewhatLongerFooBarBaz }} 3 |
4 | ----- 5 |
6 | {{ somewhatLongFooBarBaz }}/{{ somewhatLongerFooBarBaz }} 7 |
-------------------------------------------------------------------------------- /internal/html/testdata/39-attributes-escaped-content.txt: -------------------------------------------------------------------------------- 1 | 2 | ----- 3 | -------------------------------------------------------------------------------- /internal/html/testdata/40-v-if-condition.txt: -------------------------------------------------------------------------------- 1 |
2 |
3 | ----- 4 |
5 |
-------------------------------------------------------------------------------- /internal/llm/gemini.go: -------------------------------------------------------------------------------- 1 | package llm 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "strings" 8 | "time" 9 | 10 | "github.com/google/generative-ai-go/genai" 11 | "google.golang.org/api/option" 12 | 13 | "github.com/shopware/shopware-cli/logging" 14 | ) 15 | 16 | type GeminiClient struct { 17 | client *genai.Client 18 | } 19 | 20 | func newGeminiClient() (*GeminiClient, error) { 21 | apiKey := os.Getenv("GEMINI_API_KEY") 22 | 23 | if apiKey == "" { 24 | return nil, fmt.Errorf("GEMINI_API_KEY is not set") 25 | } 26 | 27 | client, err := genai.NewClient(context.Background(), option.WithAPIKey(apiKey)) 28 | if err != nil { 29 | return nil, err 30 | } 31 | 32 | return &GeminiClient{client: client}, nil 33 | } 34 | 35 | func (c *GeminiClient) Generate(ctx context.Context, prompt string, options *LLMOptions) (string, error) { 36 | resp, err := c.client.GenerativeModel(options.Model).GenerateContent(ctx, genai.Text(options.SystemPrompt+"\n\n"+prompt)) 37 | if err != nil { 38 | if strings.Contains(err.Error(), "Resource has been exhausted") { 39 | logging.FromContext(ctx).Warn("Resource exhausted, waiting 15 seconds before retrying") 40 | time.Sleep(15 * time.Second) 41 | 42 | return c.Generate(ctx, prompt, options) 43 | } 44 | 45 | return "", err 46 | } 47 | 48 | return string(resp.Candidates[0].Content.Parts[0].(genai.Text)), nil 49 | } 50 | -------------------------------------------------------------------------------- /internal/llm/main.go: -------------------------------------------------------------------------------- 1 | package llm 2 | 3 | import "context" 4 | 5 | type LLMOptions struct { 6 | Model string 7 | SystemPrompt string 8 | } 9 | 10 | type LLMClient interface { 11 | Generate(ctx context.Context, prompt string, options *LLMOptions) (string, error) 12 | } 13 | 14 | func NewLLMClient(provider string) (LLMClient, error) { 15 | switch provider { 16 | case "ollama": 17 | return newOpenAIClient(), nil 18 | case "openai": 19 | return newOpenAIClient(), nil 20 | case "gemini": 21 | return newGeminiClient() 22 | case "openrouter": 23 | return newOpenRouterClient() 24 | } 25 | 26 | panic("Invalid provider: " + provider) 27 | } 28 | -------------------------------------------------------------------------------- /internal/packagist/auth.go: -------------------------------------------------------------------------------- 1 | package packagist 2 | 3 | import ( 4 | "encoding/json" 5 | "os" 6 | ) 7 | 8 | type ComposerAuthHttpBasic struct { 9 | Username string `json:"username"` 10 | Password string `json:"password"` 11 | } 12 | 13 | type ComposerAuth struct { 14 | path string `json:"-"` 15 | HTTPBasicAuth map[string]ComposerAuthHttpBasic `json:"http-basic,omitempty"` 16 | BearerAuth map[string]string `json:"bearer,omitempty"` 17 | GitlabAuth map[string]string `json:"gitlab-token,omitempty"` 18 | GithubOAuth map[string]string `json:"github-oauth,omitempty"` 19 | BitbucketOauth map[string]map[string]string `json:"bitbucket-oauth,omitempty"` 20 | } 21 | 22 | func (a *ComposerAuth) Save() error { 23 | content, err := json.MarshalIndent(a, "", " ") 24 | if err != nil { 25 | return err 26 | } 27 | 28 | return os.WriteFile(a.path, content, os.ModePerm) 29 | } 30 | 31 | func fillAuthStruct(auth *ComposerAuth) *ComposerAuth { 32 | if auth.BearerAuth == nil { 33 | auth.BearerAuth = map[string]string{} 34 | } 35 | 36 | if auth.HTTPBasicAuth == nil { 37 | auth.HTTPBasicAuth = map[string]ComposerAuthHttpBasic{} 38 | } 39 | 40 | if auth.GitlabAuth == nil { 41 | auth.GitlabAuth = map[string]string{} 42 | } 43 | 44 | if auth.GithubOAuth == nil { 45 | auth.GithubOAuth = map[string]string{} 46 | } 47 | 48 | if auth.BitbucketOauth == nil { 49 | auth.BitbucketOauth = map[string]map[string]string{} 50 | } 51 | 52 | return auth 53 | } 54 | 55 | func ReadComposerAuth(authFile string, fallback bool) (*ComposerAuth, error) { 56 | content, err := os.ReadFile(authFile) 57 | if err != nil { 58 | if fallback { 59 | auth := fillAuthStruct(&ComposerAuth{}) 60 | auth.path = authFile 61 | 62 | return auth, nil 63 | } 64 | 65 | return nil, err 66 | } 67 | 68 | var auth ComposerAuth 69 | if err := json.Unmarshal(content, &auth); err != nil { 70 | return nil, err 71 | } 72 | 73 | auth.path = authFile 74 | 75 | return fillAuthStruct(&auth), nil 76 | } 77 | -------------------------------------------------------------------------------- /internal/packagist/lock.go: -------------------------------------------------------------------------------- 1 | package packagist 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | ) 8 | 9 | type ComposerLockPackage struct { 10 | Name string `json:"name"` 11 | Version string `json:"version"` 12 | } 13 | 14 | type ComposerLock struct { 15 | Packages []ComposerLockPackage `json:"packages"` 16 | } 17 | 18 | func (c *ComposerLock) GetPackage(name string) *ComposerLockPackage { 19 | for _, pkg := range c.Packages { 20 | if pkg.Name == name { 21 | return &pkg 22 | } 23 | } 24 | 25 | return nil 26 | } 27 | 28 | func ReadComposerLock(pathToFile string) (*ComposerLock, error) { 29 | content, err := os.ReadFile(pathToFile) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | var lock ComposerLock 35 | if err := json.Unmarshal(content, &lock); err != nil { 36 | return nil, fmt.Errorf("could not parse composer.lock: %w", err) 37 | } 38 | 39 | return &lock, nil 40 | } 41 | -------------------------------------------------------------------------------- /internal/packagist/lock_test.go: -------------------------------------------------------------------------------- 1 | package packagist 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestReadComposerLock(t *testing.T) { 12 | t.Run("valid composer.lock", func(t *testing.T) { 13 | // Create a temporary composer.lock file 14 | dir := t.TempDir() 15 | lockFile := filepath.Join(dir, "composer.lock") 16 | content := `{ 17 | "packages": [ 18 | { 19 | "name": "symfony/console", 20 | "version": "v6.3.0", 21 | "type": "library" 22 | } 23 | ] 24 | }` 25 | err := os.WriteFile(lockFile, []byte(content), 0o644) 26 | assert.NoError(t, err) 27 | 28 | // Test reading the file 29 | lock, err := ReadComposerLock(lockFile) 30 | assert.NoError(t, err) 31 | assert.NotNil(t, lock) 32 | assert.Len(t, lock.Packages, 1) 33 | assert.Equal(t, "symfony/console", lock.Packages[0].Name) 34 | assert.Equal(t, "v6.3.0", lock.Packages[0].Version) 35 | }) 36 | 37 | t.Run("non-existent file", func(t *testing.T) { 38 | lock, err := ReadComposerLock("non-existent-file.lock") 39 | assert.Error(t, err) 40 | assert.Nil(t, lock) 41 | }) 42 | 43 | t.Run("invalid JSON", func(t *testing.T) { 44 | // Create a temporary file with invalid JSON 45 | dir := t.TempDir() 46 | lockFile := filepath.Join(dir, "invalid.lock") 47 | err := os.WriteFile(lockFile, []byte("invalid json"), 0o644) 48 | assert.NoError(t, err) 49 | 50 | lock, err := ReadComposerLock(lockFile) 51 | assert.Error(t, err) 52 | assert.Nil(t, lock) 53 | }) 54 | } 55 | -------------------------------------------------------------------------------- /internal/packagist/packagist.go: -------------------------------------------------------------------------------- 1 | package packagist 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | "strings" 9 | 10 | "github.com/shopware/shopware-cli/logging" 11 | ) 12 | 13 | type PackageResponse struct { 14 | Packages map[string]map[string]PackageVersion `json:"packages"` 15 | } 16 | 17 | func (p *PackageResponse) HasPackage(name string) bool { 18 | expectedName := fmt.Sprintf("store.shopware.com/%s", strings.ToLower(name)) 19 | 20 | _, ok := p.Packages[expectedName] 21 | 22 | return ok 23 | } 24 | 25 | type PackageVersion struct { 26 | Version string `json:"version"` 27 | Replace map[string]string `json:"replace"` 28 | } 29 | 30 | func GetPackages(ctx context.Context, token string) (*PackageResponse, error) { 31 | req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://packages.shopware.com/packages.json", nil) 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | req.Header.Set("User-Agent", "Shopware CLI") 37 | req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) 38 | 39 | resp, err := http.DefaultClient.Do(req) 40 | if err != nil { 41 | return nil, err 42 | } 43 | 44 | defer func() { 45 | if err := resp.Body.Close(); err != nil { 46 | logging.FromContext(ctx).Errorf("Cannot close response body: %v", err) 47 | } 48 | }() 49 | 50 | if resp.StatusCode != http.StatusOK { 51 | return nil, fmt.Errorf("failed to get packages: %s", resp.Status) 52 | } 53 | 54 | var packages PackageResponse 55 | if err := json.NewDecoder(resp.Body).Decode(&packages); err != nil { 56 | return nil, err 57 | } 58 | 59 | return &packages, nil 60 | } 61 | -------------------------------------------------------------------------------- /internal/phpexec/phpexec.go: -------------------------------------------------------------------------------- 1 | package phpexec 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "os/exec" 7 | "sync" 8 | ) 9 | 10 | type allowBinCIKey struct{} 11 | 12 | func AllowBinCI(ctx context.Context) context.Context { 13 | return context.WithValue(ctx, allowBinCIKey{}, true) 14 | } 15 | 16 | var isCI = sync.OnceValue(func() bool { 17 | return os.Getenv("CI") != "" 18 | }) 19 | 20 | var pathToSymfonyCLI = sync.OnceValue(func() string { 21 | path, err := exec.LookPath("symfony") 22 | if err != nil { 23 | return "" 24 | } 25 | return path 26 | }) 27 | 28 | func symfonyCliAllowed() bool { 29 | return os.Getenv("SHOPWARE_CLI_NO_SYMFONY_CLI") != "1" 30 | } 31 | 32 | func ConsoleCommand(ctx context.Context, args ...string) *exec.Cmd { 33 | consoleCommand := "bin/console" 34 | 35 | if _, ok := ctx.Value(allowBinCIKey{}).(bool); ok && isCI() { 36 | consoleCommand = "bin/ci" 37 | } 38 | 39 | if path := pathToSymfonyCLI(); path != "" && symfonyCliAllowed() { 40 | return exec.CommandContext(ctx, path, append([]string{"php", consoleCommand}, args...)...) 41 | } 42 | return exec.CommandContext(ctx, "php", append([]string{consoleCommand}, args...)...) 43 | } 44 | 45 | func ComposerCommand(ctx context.Context, args ...string) *exec.Cmd { 46 | if path := pathToSymfonyCLI(); path != "" && symfonyCliAllowed() { 47 | return exec.CommandContext(ctx, path, append([]string{"composer"}, args...)...) 48 | } 49 | return exec.CommandContext(ctx, "composer", args...) 50 | } 51 | 52 | func PHPCommand(ctx context.Context, args ...string) *exec.Cmd { 53 | if path := pathToSymfonyCLI(); path != "" && symfonyCliAllowed() { 54 | return exec.CommandContext(ctx, path, append([]string{"php"}, args...)...) 55 | } 56 | return exec.CommandContext(ctx, "php", args...) 57 | } 58 | -------------------------------------------------------------------------------- /internal/phpexec/phpexec_test.go: -------------------------------------------------------------------------------- 1 | package phpexec 2 | 3 | import ( 4 | "context" 5 | "os/exec" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestSymfonyDetection(t *testing.T) { 12 | testCases := []struct { 13 | Name string 14 | Func func(context.Context, ...string) *exec.Cmd 15 | Args []string 16 | SymfonyArgs []string 17 | }{ 18 | { 19 | Name: "Composer", 20 | Func: ComposerCommand, 21 | Args: []string{"composer"}, 22 | SymfonyArgs: []string{"/test/symfony", "composer"}, 23 | }, 24 | { 25 | Name: "Console", 26 | Func: ConsoleCommand, 27 | Args: []string{"php", "bin/console"}, 28 | SymfonyArgs: []string{"/test/symfony", "php", "bin/console"}, 29 | }, 30 | { 31 | Name: "PHP", 32 | Func: PHPCommand, 33 | Args: []string{"php"}, 34 | SymfonyArgs: []string{"/test/symfony", "php"}, 35 | }, 36 | } 37 | 38 | ctx := t.Context() 39 | 40 | for _, tc := range testCases { 41 | tc := tc 42 | 43 | t.Run(tc.Name, func(t *testing.T) { 44 | t.Run("Default", func(t *testing.T) { 45 | pathToSymfonyCLI = func() string { return "" } 46 | 47 | cmd := tc.Func(ctx, "some", "arguments") 48 | assert.Equal(t, append(tc.Args, "some", "arguments"), cmd.Args) 49 | }) 50 | 51 | t.Run("Symfony", func(t *testing.T) { 52 | pathToSymfonyCLI = func() string { return "/test/symfony" } 53 | 54 | cmd := tc.Func(ctx, "some", "arguments") 55 | assert.Equal(t, append(tc.SymfonyArgs, "some", "arguments"), cmd.Args) 56 | }) 57 | 58 | t.Run("Symfony disabled", func(t *testing.T) { 59 | t.Setenv("SHOPWARE_CLI_NO_SYMFONY_CLI", "1") 60 | 61 | pathToSymfonyCLI = func() string { return "/test/symfony" } 62 | 63 | cmd := tc.Func(ctx, "some", "arguments") 64 | assert.Equal(t, append(tc.Args, "some", "arguments"), cmd.Args) 65 | }) 66 | }) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /internal/phplint/download.go: -------------------------------------------------------------------------------- 1 | package phplint 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "os" 9 | "path" 10 | 11 | "github.com/shopware/shopware-cli/internal/system" 12 | "github.com/shopware/shopware-cli/logging" 13 | ) 14 | 15 | func findPHPWasmFile(ctx context.Context, phpVersion string) ([]byte, error) { 16 | expectedFile := "php-" + phpVersion + ".wasm" 17 | expectedPathLocation := path.Join(system.GetShopwareCliCacheDir(), "wasm", "php", expectedFile) 18 | 19 | if _, err := os.Stat(expectedPathLocation); err == nil { 20 | return os.ReadFile(expectedPathLocation) 21 | } 22 | 23 | downloadUrl := "https://github.com/shopwareLabs/php-cli-wasm-binaries/releases/download/1.0.0/" + expectedFile 24 | 25 | r, err := http.NewRequestWithContext(ctx, http.MethodGet, downloadUrl, nil) 26 | if err != nil { 27 | return nil, err 28 | } 29 | 30 | r.Header.Set("accept", "application/octet-stream") 31 | 32 | resp, err := http.DefaultClient.Do(r) 33 | if err != nil { 34 | return nil, err 35 | } 36 | 37 | if resp.StatusCode != 200 { 38 | return nil, fmt.Errorf("cannot download php-wasm binary: %s (%s)", resp.Status, downloadUrl) 39 | } 40 | 41 | if _, err := os.Stat(path.Dir(expectedPathLocation)); os.IsNotExist(err) { 42 | if err := os.MkdirAll(path.Dir(expectedPathLocation), os.ModePerm); err != nil { 43 | return nil, fmt.Errorf("cannot create directory %s: %v", path.Dir(expectedPathLocation), err) 44 | } 45 | } 46 | 47 | data, err := io.ReadAll(resp.Body) 48 | if err != nil { 49 | _ = resp.Body.Close() 50 | 51 | return nil, fmt.Errorf("findPHPWasmFile: %v", err) 52 | } 53 | 54 | _ = resp.Body.Close() 55 | 56 | if err := os.WriteFile(expectedPathLocation, data, os.ModePerm); err != nil { 57 | logging.FromContext(ctx).Debugf("cannot write php-wasm binary to %s: %v", expectedPathLocation, err) 58 | } 59 | 60 | return data, nil 61 | } 62 | -------------------------------------------------------------------------------- /internal/phplint/download_test.go: -------------------------------------------------------------------------------- 1 | package phplint 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestDownloadPHPFile(t *testing.T) { 11 | if os.Getenv("NIX_CC") != "" { 12 | t.Skip("Downloading does not work in Nix build") 13 | } 14 | 15 | _, err := findPHPWasmFile(t.Context(), "7.4") 16 | assert.NoError(t, err) 17 | } 18 | -------------------------------------------------------------------------------- /internal/phplint/lint_test.go: -------------------------------------------------------------------------------- 1 | package phplint 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestLintTestData(t *testing.T) { 11 | if os.Getenv("NIX_CC") != "" { 12 | t.Skip("Downloading does not work in Nix build") 13 | } 14 | 15 | supportedPHPVersions := []string{"7.3", "7.4", "8.1", "8.2", "8.3"} 16 | 17 | for _, version := range supportedPHPVersions { 18 | errors, err := LintFolder(t.Context(), version, "testdata") 19 | 20 | assert.NoError(t, err) 21 | 22 | assert.Len(t, errors, 1) 23 | 24 | assert.Equal(t, "invalid.php", errors[0].File) 25 | 26 | if version == "7.3" { 27 | assert.Contains(t, errors[0].Message, "Errors parsing invalid.php") 28 | } else { 29 | assert.Contains(t, errors[0].Message, "syntax error, unexpected end of file") 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /internal/phplint/testdata/invalid.php: -------------------------------------------------------------------------------- 1 | 0 && version[0] == 'v' { 29 | version = version[1:] 30 | } 31 | 32 | return version, nil 33 | } 34 | 35 | // IsNodeVersionAtLeast checks if the installed Node.js version meets the minimum required version. 36 | func IsNodeVersionAtLeast(requiredVersion string) (bool, error) { 37 | installedVersion, err := GetInstalledNodeVersion() 38 | if err != nil { 39 | return false, err 40 | } 41 | 42 | nodeVersion, err := version.NewVersion(installedVersion) 43 | if err != nil { 44 | return false, fmt.Errorf("failed to parse installed Node.js version: %w", err) 45 | } 46 | 47 | constraint, err := version.NewConstraint(fmt.Sprintf(">= %s", requiredVersion)) 48 | if err != nil { 49 | return false, fmt.Errorf("failed to parse required Node.js version constraint: %w", err) 50 | } 51 | 52 | return constraint.Check(nodeVersion), nil 53 | } 54 | -------------------------------------------------------------------------------- /internal/system/node_test.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestGetNodeVersionNotInstalled(t *testing.T) { 13 | t.Setenv("PATH", "") 14 | _, err := GetInstalledNodeVersion() 15 | assert.ErrorContains(t, err, "node.js is not installed") 16 | } 17 | 18 | func TestGetNodeVersion(t *testing.T) { 19 | tmpDir := t.TempDir() 20 | 21 | setupFakeNode(t, tmpDir, "18.16.0") 22 | 23 | nodeVersion, err := GetInstalledNodeVersion() 24 | assert.NoError(t, err) 25 | assert.Equal(t, "18.16.0", nodeVersion) 26 | } 27 | 28 | func TestNodeVersionIsAtLeast(t *testing.T) { 29 | setupFakeNode(t, t.TempDir(), "18.16.0") 30 | hit, err := IsNodeVersionAtLeast("16.0.0") 31 | 32 | assert.NoError(t, err) 33 | assert.True(t, hit, "node.js version should be at least 16.0.0") 34 | } 35 | 36 | func TestNodeVersionIsNotAtLeast(t *testing.T) { 37 | setupFakeNode(t, t.TempDir(), "14.17.0") 38 | hit, err := IsNodeVersionAtLeast("16.0.0") 39 | 40 | assert.NoError(t, err) 41 | assert.False(t, hit, "node.js version should not be at least 16.0.0") 42 | } 43 | 44 | func setupFakeNode(t *testing.T, tmpDir string, version string) { 45 | t.Helper() 46 | shPath, err := exec.LookPath("sh") 47 | assert.NoError(t, err) 48 | 49 | // Node returns version with 'v' prefix 50 | assert.NoError(t, os.WriteFile(tmpDir+"/node", []byte(fmt.Sprintf("#!%s\necho v%s", shPath, version)), 0755)) 51 | t.Setenv("PATH", tmpDir) 52 | } 53 | -------------------------------------------------------------------------------- /internal/system/php.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "fmt" 5 | "os/exec" 6 | "strings" 7 | 8 | "github.com/shyim/go-version" 9 | ) 10 | 11 | // GetInstalledPHPVersion checks the installed PHP version on the system. 12 | func GetInstalledPHPVersion() (string, error) { 13 | // Check if PHP is installed 14 | phpPath, err := exec.LookPath("php") 15 | if err != nil { 16 | return "", fmt.Errorf("PHP is not installed: %w", err) 17 | } 18 | 19 | // Get the PHP version 20 | cmd := exec.Command(phpPath, "-v") 21 | output, err := cmd.Output() 22 | if err != nil { 23 | return "", fmt.Errorf("failed to get PHP version: %w, output: %s", err, string(output)) 24 | } 25 | 26 | splitt := strings.Split(string(output), " ") 27 | 28 | if len(splitt) < 2 { 29 | return "", fmt.Errorf("unexpected output format: %s", string(output)) 30 | } 31 | 32 | // Parse the version from the output 33 | version := splitt[1] 34 | return strings.TrimSpace(version), nil 35 | } 36 | 37 | func IsPHPVersionAtLeast(requiredVersion string) (bool, error) { 38 | installedVersion, err := GetInstalledPHPVersion() 39 | if err != nil { 40 | return false, err 41 | } 42 | 43 | phpVersion, err := version.NewVersion(installedVersion) 44 | if err != nil { 45 | return false, fmt.Errorf("failed to parse installed PHP version: %w", err) 46 | } 47 | 48 | constraint, err := version.NewConstraint(fmt.Sprintf(">= %s", requiredVersion)) 49 | if err != nil { 50 | return false, fmt.Errorf("failed to parse required PHP version constraint: %w", err) 51 | } 52 | 53 | return constraint.Check(phpVersion), nil 54 | } 55 | -------------------------------------------------------------------------------- /internal/system/php_test.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestGetPHPVersionNotInstalled(t *testing.T) { 13 | t.Setenv("PATH", "") 14 | _, err := GetInstalledPHPVersion() 15 | assert.ErrorContains(t, err, "PHP is not installed") 16 | } 17 | 18 | func TestGetPHPVersion(t *testing.T) { 19 | tmpDir := t.TempDir() 20 | 21 | setupFakePHP(t, tmpDir, "8.0.0") 22 | 23 | phpVersion, err := GetInstalledPHPVersion() 24 | assert.NoError(t, err) 25 | assert.Equal(t, "8.0.0", phpVersion) 26 | } 27 | 28 | func TestPHPVersionIsAtLeast(t *testing.T) { 29 | setupFakePHP(t, t.TempDir(), "8.0.0") 30 | hit, err := IsPHPVersionAtLeast("8.0.0") 31 | 32 | assert.NoError(t, err) 33 | assert.True(t, hit, "PHP version should be at least 8.0.0") 34 | } 35 | 36 | func TestPHPVersionIsNotAtLeast(t *testing.T) { 37 | setupFakePHP(t, t.TempDir(), "7.4.0") 38 | hit, err := IsPHPVersionAtLeast("8.0.0") 39 | 40 | assert.NoError(t, err) 41 | assert.False(t, hit, "PHP version should not be at least 8.0.0") 42 | } 43 | 44 | func setupFakePHP(t *testing.T, tmpDir string, version string) { 45 | t.Helper() 46 | shPath, err := exec.LookPath("sh") 47 | assert.NoError(t, err) 48 | 49 | assert.NoError(t, os.WriteFile(tmpDir+"/php", []byte(fmt.Sprintf("#!%s\necho PHP %s", shPath, version)), 0755)) 50 | t.Setenv("PATH", tmpDir) 51 | } 52 | -------------------------------------------------------------------------------- /internal/twigparser/parser_types.go: -------------------------------------------------------------------------------- 1 | package twigparser 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | ) 7 | 8 | // parseTypes parses the content of a types tag. 9 | // For example, given "score: 'number'" it returns a TypesNode with the mapping. 10 | func parseTypes(content string) (Node, error) { 11 | typesMap := make(map[string]string) 12 | if strings.TrimSpace(content) == "" { 13 | return nil, errors.New("no types provided") 14 | } 15 | // For simplicity, assume tokens do not contain spaces. 16 | tokens := strings.Fields(content) 17 | for _, token := range tokens { 18 | parts := strings.SplitN(token, ":", 2) 19 | if len(parts) != 2 { 20 | return nil, errors.New("invalid types format") 21 | } 22 | key := strings.TrimSpace(parts[0]) 23 | value := strings.TrimSpace(parts[1]) 24 | // Remove quotes if present. 25 | if len(value) >= 2 && ((value[0] == '\'' && value[len(value)-1] == '\'') || (value[0] == '"' && value[len(value)-1] == '"')) { 26 | value = value[1 : len(value)-1] 27 | } 28 | // Preserve quotes in dump. 29 | typesMap[key] = "'" + value + "'" 30 | } 31 | return &TypesNode{Types: typesMap}, nil 32 | } 33 | -------------------------------------------------------------------------------- /internal/verifier/admintwiglinter/constants.go: -------------------------------------------------------------------------------- 1 | package admintwiglinter 2 | 3 | // Common string constants used across the package 4 | const ( 5 | // File extensions 6 | TwigExtension = ".twig" 7 | 8 | // Attribute values 9 | CriticalValue = "critical" 10 | MediumValue = "medium" 11 | DefaultValue = "default" 12 | 13 | // Element tags 14 | TemplateTag = "template" 15 | 16 | // Attribute keys 17 | ValueAttr = "value" 18 | SizeAttr = "size" 19 | ColonValueAttr = ":value" 20 | VModelValueAttr = "v-model:value" 21 | VModelAttr = "v-model" 22 | ModelValueAttr = "model-value" 23 | UpdateValueAttr = "@update:value" 24 | UpdateModelValueAttr = "@update:model-value" 25 | LabelSlotAttr = "#label" 26 | HintSlotAttr = "#hint" 27 | BaseFieldMountedAttr = "@base-field-mounted" 28 | IsInvalidAttr = "isInvalid" 29 | AiBadgeAttr = "aiBadge" 30 | ) 31 | -------------------------------------------------------------------------------- /internal/verifier/admintwiglinter/fix_alert.go: -------------------------------------------------------------------------------- 1 | package admintwiglinter 2 | 3 | import ( 4 | "github.com/shyim/go-version" 5 | 6 | "github.com/shopware/shopware-cli/internal/html" 7 | ) 8 | 9 | type AlertFixer struct{} 10 | 11 | func init() { 12 | AddFixer(AlertFixer{}) 13 | } 14 | 15 | func (a AlertFixer) Check(nodes []html.Node) []CheckError { 16 | var errors []CheckError 17 | html.TraverseNode(nodes, func(node *html.ElementNode) { 18 | if node.Tag == "sw-alert" { 19 | errors = append(errors, CheckError{ 20 | Message: "sw-alert is removed, use mt-banner instead. Please review conversion for variant changes.", 21 | Severity: "error", 22 | Identifier: "sw-alert", 23 | Line: node.Line, 24 | }) 25 | } 26 | }) 27 | return errors 28 | } 29 | 30 | func (a AlertFixer) Supports(v *version.Version) bool { 31 | return shopware67Constraint.Check(v) 32 | } 33 | 34 | func (a AlertFixer) Fix(nodes []html.Node) error { 35 | html.TraverseNode(nodes, func(node *html.ElementNode) { 36 | if node.Tag == "sw-alert" { 37 | node.Tag = "mt-banner" 38 | var newAttrs html.NodeList 39 | 40 | for _, attrNode := range node.Attributes { 41 | // Check if the attribute is an html.Attribute 42 | if attr, ok := attrNode.(html.Attribute); ok { 43 | if attr.Key == "variant" { 44 | switch attr.Value { 45 | case "success": 46 | attr.Value = "positive" 47 | newAttrs = append(newAttrs, attr) 48 | case "error": 49 | attr.Value = "critical" 50 | newAttrs = append(newAttrs, attr) 51 | case "warning": 52 | attr.Value = "attention" 53 | newAttrs = append(newAttrs, attr) 54 | case "info": 55 | // Keep info as is 56 | newAttrs = append(newAttrs, attr) 57 | default: 58 | // Keep any other variants unchanged 59 | newAttrs = append(newAttrs, attr) 60 | } 61 | } else { 62 | // Preserve all other attributes 63 | newAttrs = append(newAttrs, attr) 64 | } 65 | } else { 66 | // If it's not an html.Attribute (e.g., TwigIfNode), preserve it as is 67 | newAttrs = append(newAttrs, attrNode) 68 | } 69 | } 70 | 71 | node.Attributes = newAttrs 72 | } 73 | }) 74 | return nil 75 | } 76 | -------------------------------------------------------------------------------- /internal/verifier/admintwiglinter/fix_alert_test.go: -------------------------------------------------------------------------------- 1 | package admintwiglinter 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestAlertFixer(t *testing.T) { 10 | cases := []struct { 11 | description string 12 | before string 13 | after string 14 | }{ 15 | { 16 | description: "basic component replacement", 17 | before: `Message`, 18 | after: `Message`, 19 | }, 20 | { 21 | description: "info variant remains unchanged", 22 | before: `Info message`, 23 | after: `Info message`, 24 | }, 25 | { 26 | description: "success variant converts to positive", 27 | before: `Success message`, 28 | after: `Success message`, 29 | }, 30 | { 31 | description: "error variant converts to critical", 32 | before: `Error message`, 33 | after: `Error message`, 34 | }, 35 | { 36 | description: "warning variant converts to attention", 37 | before: `Warning message`, 38 | after: `Warning message`, 39 | }, 40 | { 41 | description: "preserve other attributes", 42 | before: `Error message`, 43 | after: `Error message`, 48 | }, 49 | } 50 | 51 | for _, c := range cases { 52 | newStr, err := runFixerOnString(AlertFixer{}, c.before) 53 | assert.NoError(t, err, c.description) 54 | assert.Equal(t, c.after, newStr, c.description) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /internal/verifier/admintwiglinter/fix_button_test.go: -------------------------------------------------------------------------------- 1 | package admintwiglinter 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestButtonFixer(t *testing.T) { 10 | cases := []struct { 11 | description string 12 | before string 13 | after string 14 | }{ 15 | { 16 | description: "basic component replacement", 17 | before: `Save`, 18 | after: `Save`, 19 | }, 20 | { 21 | description: "remove variant ghost and add ghost attribute", 22 | before: `Save`, 23 | after: `Save`, 24 | }, 25 | { 26 | description: "replace danger variant with critical", 27 | before: `Delete`, 28 | after: `Delete`, 29 | }, 30 | { 31 | description: "replace ghost-danger variant with critical and add ghost", 32 | before: `Delete`, 33 | after: `Delete`, 37 | }, 38 | { 39 | description: "remove contrast variant", 40 | before: `Info`, 41 | after: `Info`, 42 | }, 43 | { 44 | description: "remove context variant", 45 | before: `Info`, 46 | after: `Info`, 47 | }, 48 | { 49 | description: "replace router-link with @click", 50 | before: `Go to example`, 51 | after: `Go to example`, 52 | }, 53 | } 54 | 55 | for _, c := range cases { 56 | newStr, err := runFixerOnString(ButtonFixer{}, c.before) 57 | assert.NoError(t, err, c.description) 58 | assert.Equal(t, c.after, newStr, c.description) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /internal/verifier/admintwiglinter/fix_card_test.go: -------------------------------------------------------------------------------- 1 | package admintwiglinter 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestCardFixer(t *testing.T) { 10 | cases := []struct { 11 | description string 12 | before string 13 | after string 14 | }{ 15 | { 16 | description: "basic component replacement", 17 | before: `Hello World`, 18 | after: `Hello World`, 19 | }, 20 | { 21 | description: "remove contentPadding property", 22 | before: `Hello World`, 23 | after: `Hello World`, 24 | }, 25 | { 26 | description: "convert aiBadge property to title slot", 27 | before: `Hello World`, 28 | after: ` 29 | 30 | 31 | 32 | Hello World 33 | `, 34 | }, 35 | } 36 | 37 | for _, c := range cases { 38 | newStr, err := runFixerOnString(CardFixer{}, c.before) 39 | assert.NoError(t, err, c.description) 40 | assert.Equal(t, c.after, newStr, c.description) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /internal/verifier/admintwiglinter/fix_colorpicker_test.go: -------------------------------------------------------------------------------- 1 | package admintwiglinter 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestColorpickerFixer(t *testing.T) { 10 | cases := []struct { 11 | description string 12 | before string 13 | after string 14 | }{ 15 | { 16 | description: "basic component replacement", 17 | before: ``, 18 | after: ``, 19 | }, 20 | { 21 | description: "replace value with model-value", 22 | before: ``, 23 | after: ``, 24 | }, 25 | { 26 | description: "replace v-model:value with v-model", 27 | before: ``, 28 | after: ``, 29 | }, 30 | { 31 | description: "replace update:value event", 32 | before: ``, 33 | after: ``, 34 | }, 35 | { 36 | description: "process label slot", 37 | before: ``, 38 | after: ``, 39 | }, 40 | } 41 | 42 | for _, c := range cases { 43 | newStr, err := runFixerOnString(ColorpickerFixer{}, c.before) 44 | assert.NoError(t, err, c.description) 45 | assert.Equal(t, c.after, newStr, c.description) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /internal/verifier/admintwiglinter/fix_datepicker_test.go: -------------------------------------------------------------------------------- 1 | package admintwiglinter 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestDatepickerFixer(t *testing.T) { 10 | cases := []struct { 11 | description string 12 | before string 13 | after string 14 | }{ 15 | { 16 | description: "basic component replacement", 17 | before: ``, 18 | after: ``, 23 | }, 24 | { 25 | description: "convert label slot to prop", 26 | before: ``, 27 | after: ``, 28 | }, 29 | } 30 | 31 | for _, c := range cases { 32 | newStr, err := runFixerOnString(DatepickerFixer{}, c.before) 33 | assert.NoError(t, err, c.description) 34 | assert.Equal(t, c.after, newStr, c.description) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /internal/verifier/admintwiglinter/fix_email_field_test.go: -------------------------------------------------------------------------------- 1 | package admintwiglinter 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestEmailFieldFixer(t *testing.T) { 10 | cases := []struct { 11 | description string 12 | before string 13 | after string 14 | }{ 15 | { 16 | description: "basic component replacement", 17 | before: ``, 18 | after: ``, 19 | }, 20 | { 21 | description: "replace value with model-value", 22 | before: ``, 23 | after: ``, 24 | }, 25 | { 26 | description: "replace v-model:value with v-model", 27 | before: ``, 28 | after: ``, 29 | }, 30 | { 31 | description: "replace size medium with default", 32 | before: ``, 33 | after: ``, 34 | }, 35 | { 36 | description: "remove isInvalid attribute", 37 | before: ``, 38 | after: ``, 39 | }, 40 | { 41 | description: "remove aiBadge attribute", 42 | before: ``, 43 | after: ``, 44 | }, 45 | { 46 | description: "replace update:value event", 47 | before: ``, 48 | after: ``, 49 | }, 50 | { 51 | description: "remove base-field-mounted event", 52 | before: ``, 53 | after: ``, 54 | }, 55 | { 56 | description: "process label slot", 57 | before: ` 58 | 61 | `, 62 | after: ``, 63 | }, 64 | } 65 | 66 | for _, c := range cases { 67 | newStr, err := runFixerOnString(EmailFieldFixer{}, c.before) 68 | assert.NoError(t, err, c.description) 69 | assert.Equal(t, c.after, newStr, c.description) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /internal/verifier/admintwiglinter/fix_external_link.go: -------------------------------------------------------------------------------- 1 | package admintwiglinter 2 | 3 | import ( 4 | "github.com/shyim/go-version" 5 | 6 | "github.com/shopware/shopware-cli/internal/html" 7 | ) 8 | 9 | type ExternalLinkFixer struct{} 10 | 11 | func init() { 12 | AddFixer(ExternalLinkFixer{}) 13 | } 14 | 15 | func (e ExternalLinkFixer) Check(nodes []html.Node) []CheckError { 16 | var checkErrors []CheckError 17 | html.TraverseNode(nodes, func(node *html.ElementNode) { 18 | if node.Tag == "sw-external-link" { 19 | checkErrors = append(checkErrors, CheckError{ 20 | Message: "sw-external-link is removed, use mt-external-link instead and remove the icon property.", 21 | Severity: "error", 22 | Identifier: "sw-external-link", 23 | Line: node.Line, 24 | }) 25 | } 26 | }) 27 | return checkErrors 28 | } 29 | 30 | func (e ExternalLinkFixer) Supports(v *version.Version) bool { 31 | return shopware67Constraint.Check(v) 32 | } 33 | 34 | func (e ExternalLinkFixer) Fix(nodes []html.Node) error { 35 | html.TraverseNode(nodes, func(node *html.ElementNode) { 36 | if node.Tag == "sw-external-link" { 37 | node.Tag = "mt-external-link" 38 | var newAttrs html.NodeList 39 | for _, attrNode := range node.Attributes { 40 | // Check if the attribute is an html.Attribute 41 | if attr, ok := attrNode.(html.Attribute); ok { 42 | if attr.Key == "icon" { 43 | continue 44 | } 45 | newAttrs = append(newAttrs, attr) 46 | } else { 47 | // If it's not an html.Attribute (e.g., TwigIfNode), preserve it as is 48 | newAttrs = append(newAttrs, attrNode) 49 | } 50 | } 51 | node.Attributes = newAttrs 52 | } 53 | }) 54 | return nil 55 | } 56 | -------------------------------------------------------------------------------- /internal/verifier/admintwiglinter/fix_external_link_test.go: -------------------------------------------------------------------------------- 1 | package admintwiglinter 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestExternalLinkFixer(t *testing.T) { 10 | cases := []struct { 11 | description string 12 | before string 13 | after string 14 | }{ 15 | { 16 | description: "basic component replacement", 17 | before: `Hello World`, 18 | after: `Hello World`, 19 | }, 20 | { 21 | description: "remove icon attribute", 22 | before: `Hello World`, 23 | after: `Hello World`, 24 | }, 25 | } 26 | 27 | for _, c := range cases { 28 | newStr, err := runFixerOnString(ExternalLinkFixer{}, c.before) 29 | assert.NoError(t, err, c.description) 30 | assert.Equal(t, c.after, newStr, c.description) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /internal/verifier/admintwiglinter/fix_icon_test.go: -------------------------------------------------------------------------------- 1 | package admintwiglinter 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestIconFixer(t *testing.T) { 10 | cases := []struct { 11 | description string 12 | before string 13 | after string 14 | }{ 15 | { 16 | description: "basic component replacement with default size", 17 | before: ``, 18 | after: ``, 22 | }, 23 | { 24 | description: "replace small with size 16px", 25 | before: ``, 26 | after: ``, 30 | }, 31 | { 32 | description: "replace large with size 32px", 33 | before: ``, 34 | after: ``, 38 | }, 39 | } 40 | 41 | for _, c := range cases { 42 | newStr, err := runFixerOnString(IconFixer{}, c.before) 43 | assert.NoError(t, err, c.description) 44 | assert.Equal(t, c.after, newStr, c.description) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /internal/verifier/admintwiglinter/fix_loader.go: -------------------------------------------------------------------------------- 1 | package admintwiglinter 2 | 3 | import ( 4 | "github.com/shyim/go-version" 5 | 6 | "github.com/shopware/shopware-cli/internal/html" 7 | ) 8 | 9 | type LoaderFixer struct{} 10 | 11 | func init() { 12 | AddFixer(LoaderFixer{}) 13 | } 14 | 15 | func (l LoaderFixer) Check(nodes []html.Node) []CheckError { 16 | var errs []CheckError 17 | html.TraverseNode(nodes, func(node *html.ElementNode) { 18 | if node.Tag == "sw-loader" { 19 | errs = append(errs, CheckError{ 20 | Message: "sw-loader is removed, use mt-loader instead.", 21 | Severity: "error", 22 | Identifier: "sw-loader", 23 | Line: node.Line, 24 | }) 25 | } 26 | }) 27 | return errs 28 | } 29 | 30 | func (l LoaderFixer) Supports(v *version.Version) bool { 31 | return shopware67Constraint.Check(v) 32 | } 33 | 34 | func (l LoaderFixer) Fix(nodes []html.Node) error { 35 | html.TraverseNode(nodes, func(node *html.ElementNode) { 36 | if node.Tag == "sw-loader" { 37 | node.Tag = "mt-loader" 38 | } 39 | }) 40 | return nil 41 | } 42 | -------------------------------------------------------------------------------- /internal/verifier/admintwiglinter/fix_loader_test.go: -------------------------------------------------------------------------------- 1 | package admintwiglinter 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestLoaderFixer(t *testing.T) { 10 | cases := []struct { 11 | before string 12 | after string 13 | }{ 14 | { 15 | before: ``, 16 | after: ``, 17 | }, 18 | } 19 | 20 | for _, c := range cases { 21 | newStr, err := runFixerOnString(LoaderFixer{}, c.before) 22 | assert.NoError(t, err) 23 | assert.Equal(t, c.after, newStr) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /internal/verifier/admintwiglinter/fix_number_field_test.go: -------------------------------------------------------------------------------- 1 | package admintwiglinter 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestNumberFieldFixer(t *testing.T) { 10 | cases := []struct { 11 | description string 12 | before string 13 | after string 14 | }{ 15 | { 16 | description: "basic component replacement", 17 | before: ``, 18 | after: ``, 19 | }, 20 | { 21 | description: "replace :value with :model-value", 22 | before: ``, 23 | after: ``, 24 | }, 25 | { 26 | description: "convert v-model:value to :model-value and @change", 27 | before: ``, 28 | after: ``, 29 | }, 30 | { 31 | description: "convert label slot to label prop", 32 | before: ``, 33 | after: ``, 34 | }, 35 | { 36 | description: "replace @update:value with @change", 37 | before: ``, 38 | after: ``, 39 | }, 40 | } 41 | 42 | for _, c := range cases { 43 | newStr, err := runFixerOnString(NumberFieldFixer{}, c.before) 44 | assert.NoError(t, err, c.description) 45 | assert.Equal(t, c.after, newStr, c.description) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /internal/verifier/admintwiglinter/fix_progress_bar.go: -------------------------------------------------------------------------------- 1 | package admintwiglinter 2 | 3 | import ( 4 | "github.com/shyim/go-version" 5 | 6 | "github.com/shopware/shopware-cli/internal/html" 7 | ) 8 | 9 | type ProgressBarFixer struct{} 10 | 11 | func init() { 12 | AddFixer(ProgressBarFixer{}) 13 | } 14 | 15 | func (p ProgressBarFixer) Check(nodes []html.Node) []CheckError { 16 | var errors []CheckError 17 | html.TraverseNode(nodes, func(node *html.ElementNode) { 18 | if node.Tag == "sw-progress-bar" { 19 | errors = append(errors, CheckError{ 20 | Message: "sw-progress-bar is removed, use mt-progress-bar instead.", 21 | Severity: "error", 22 | Identifier: "sw-progress-bar", 23 | Line: node.Line, 24 | }) 25 | } 26 | }) 27 | return errors 28 | } 29 | 30 | func (p ProgressBarFixer) Supports(v *version.Version) bool { 31 | return shopware67Constraint.Check(v) 32 | } 33 | 34 | func (p ProgressBarFixer) Fix(nodes []html.Node) error { 35 | html.TraverseNode(nodes, func(node *html.ElementNode) { 36 | if node.Tag == "sw-progress-bar" { 37 | node.Tag = "mt-progress-bar" 38 | var newAttrs html.NodeList 39 | 40 | for _, attrNode := range node.Attributes { 41 | // Check if the attribute is an html.Attribute 42 | if attr, ok := attrNode.(html.Attribute); ok { 43 | switch attr.Key { 44 | case ValueAttr: 45 | attr.Key = ModelValueAttr 46 | newAttrs = append(newAttrs, attr) 47 | case VModelValueAttr: 48 | attr.Key = VModelAttr 49 | newAttrs = append(newAttrs, attr) 50 | case UpdateValueAttr: 51 | attr.Key = UpdateModelValueAttr 52 | newAttrs = append(newAttrs, attr) 53 | default: 54 | newAttrs = append(newAttrs, attr) 55 | } 56 | } else { 57 | // If it's not an html.Attribute (e.g., TwigIfNode), preserve it as is 58 | newAttrs = append(newAttrs, attrNode) 59 | } 60 | } 61 | node.Attributes = newAttrs 62 | } 63 | }) 64 | return nil 65 | } 66 | -------------------------------------------------------------------------------- /internal/verifier/admintwiglinter/fix_progress_bar_test.go: -------------------------------------------------------------------------------- 1 | package admintwiglinter 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestProgressBarFixer(t *testing.T) { 10 | cases := []struct { 11 | description string 12 | before string 13 | after string 14 | }{ 15 | { 16 | description: "replace value with model-value", 17 | before: ``, 18 | after: ``, 19 | }, 20 | { 21 | description: "replace v-model:value with v-model", 22 | before: ``, 23 | after: ``, 24 | }, 25 | { 26 | description: "replace update:value event", 27 | before: ``, 28 | after: ``, 29 | }, 30 | } 31 | 32 | for _, c := range cases { 33 | newStr, err := runFixerOnString(ProgressBarFixer{}, c.before) 34 | assert.NoError(t, err, c.description) 35 | assert.Equal(t, c.after, newStr, c.description) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /internal/verifier/admintwiglinter/fix_select_field_test.go: -------------------------------------------------------------------------------- 1 | package admintwiglinter 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestSelectFieldFixer(t *testing.T) { 10 | cases := []struct { 11 | description string 12 | before string 13 | after string 14 | }{ 15 | { 16 | description: "replace value with model-value", 17 | before: ``, 18 | after: ``, 19 | }, 20 | { 21 | description: "replace v-model:value with v-model", 22 | before: ``, 23 | after: ``, 24 | }, 25 | { 26 | description: "convert options prop format", 27 | before: ``, 28 | after: ``, 31 | }, 32 | { 33 | description: "convert default slot with option children to options prop", 34 | before: ` 35 | 36 | 37 | `, 38 | after: ``, 41 | }, 42 | { 43 | description: "convert label slot to label prop", 44 | before: ``, 45 | after: ``, 46 | }, 47 | { 48 | description: "replace update:value event with update:model-value", 49 | before: ``, 50 | after: ``, 51 | }, 52 | } 53 | 54 | for _, c := range cases { 55 | newStr, err := runFixerOnString(SelectFieldFixer{}, c.before) 56 | assert.NoError(t, err, c.description) 57 | assert.Equal(t, c.after, newStr, c.description) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /internal/verifier/admintwiglinter/fix_skeleton_bar.go: -------------------------------------------------------------------------------- 1 | package admintwiglinter 2 | 3 | import ( 4 | "github.com/shyim/go-version" 5 | 6 | "github.com/shopware/shopware-cli/internal/html" 7 | ) 8 | 9 | type SkeletonBarFixer struct{} 10 | 11 | func init() { 12 | AddFixer(SkeletonBarFixer{}) 13 | } 14 | 15 | func (s SkeletonBarFixer) Check(nodes []html.Node) []CheckError { 16 | var errors []CheckError 17 | html.TraverseNode(nodes, func(node *html.ElementNode) { 18 | if node.Tag == "sw-skeleton-bar" { 19 | errors = append(errors, CheckError{ 20 | Message: "sw-skeleton-bar is removed, use mt-skeleton-bar instead.", 21 | Severity: "error", 22 | Identifier: "sw-skeleton-bar", 23 | Line: node.Line, 24 | }) 25 | } 26 | }) 27 | return errors 28 | } 29 | 30 | func (s SkeletonBarFixer) Supports(v *version.Version) bool { 31 | return shopware67Constraint.Check(v) 32 | } 33 | 34 | func (s SkeletonBarFixer) Fix(nodes []html.Node) error { 35 | html.TraverseNode(nodes, func(node *html.ElementNode) { 36 | if node.Tag == "sw-skeleton-bar" { 37 | node.Tag = "mt-skeleton-bar" 38 | } 39 | }) 40 | return nil 41 | } 42 | -------------------------------------------------------------------------------- /internal/verifier/admintwiglinter/fix_skeleton_bar_test.go: -------------------------------------------------------------------------------- 1 | package admintwiglinter 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestSkeletonBarFixer(t *testing.T) { 10 | cases := []struct { 11 | description string 12 | before string 13 | after string 14 | }{ 15 | { 16 | before: `Hello World`, 17 | after: `Hello World`, 18 | }, 19 | } 20 | 21 | for _, c := range cases { 22 | newStr, err := runFixerOnString(SkeletonBarFixer{}, c.before) 23 | assert.NoError(t, err, c.description) 24 | assert.Equal(t, c.after, newStr, c.description) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /internal/verifier/admintwiglinter/fix_switch_test.go: -------------------------------------------------------------------------------- 1 | package admintwiglinter 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestSwitchFixer(t *testing.T) { 10 | cases := []struct { 11 | description string 12 | before string 13 | after string 14 | }{ 15 | { 16 | description: "basic component replacement", 17 | before: `Hello World`, 18 | after: `Hello World`, 19 | }, 20 | { 21 | description: "replace v-model:value", 22 | before: `Hello World`, 23 | after: `Hello World`, 24 | }, 25 | { 26 | description: "replace noMarginTop with removeTopMargin", 27 | before: ``, 28 | after: ``, 29 | }, 30 | { 31 | description: "replace value with checked", 32 | before: ``, 33 | after: ``, 34 | }, 35 | { 36 | description: "convert label slot to label prop", 37 | before: ``, 40 | after: ``, 41 | }, 42 | { 43 | description: "remove hint slot and add comment node", 44 | before: ``, 47 | after: ``, 48 | }, 49 | } 50 | 51 | for _, c := range cases { 52 | newStr, err := runFixerOnString(SwitchFixer{}, c.before) 53 | assert.NoError(t, err, c.description) 54 | assert.Equal(t, c.after, newStr, c.description) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /internal/verifier/admintwiglinter/fix_text_field_test.go: -------------------------------------------------------------------------------- 1 | package admintwiglinter 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestTextFieldFixer(t *testing.T) { 10 | cases := []struct { 11 | description string 12 | before string 13 | after string 14 | }{ 15 | { 16 | description: "basic component replacement", 17 | before: ``, 18 | after: ``, 19 | }, 20 | { 21 | description: "replace value with model-value", 22 | before: ``, 23 | after: ``, 24 | }, 25 | { 26 | description: "replace v-model:value with v-model", 27 | before: ``, 28 | after: ``, 29 | }, 30 | { 31 | description: "convert size medium to default", 32 | before: ``, 33 | after: ``, 34 | }, 35 | { 36 | description: "remove isInvalid prop", 37 | before: ``, 38 | after: ``, 39 | }, 40 | { 41 | description: "remove aiBadge prop", 42 | before: ``, 43 | after: ``, 44 | }, 45 | { 46 | description: "replace update:value event", 47 | before: ``, 48 | after: ``, 49 | }, 50 | { 51 | description: "remove base-field-mounted event", 52 | before: ``, 53 | after: ``, 54 | }, 55 | { 56 | description: "process label slot conversion", 57 | before: ``, 58 | after: ``, 59 | }, 60 | } 61 | 62 | for _, c := range cases { 63 | newStr, err := runFixerOnString(TextFieldFixer{}, c.before) 64 | assert.NoError(t, err, c.description) 65 | assert.Equal(t, c.after, newStr, c.description) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /internal/verifier/admintwiglinter/fix_textareafield_test.go: -------------------------------------------------------------------------------- 1 | package admintwiglinter 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestTextarea(t *testing.T) { 10 | cases := []struct { 11 | description string 12 | before string 13 | after string 14 | }{ 15 | { 16 | description: "basic component replacement", 17 | before: ``, 18 | after: ``, 19 | }, 20 | } 21 | 22 | for _, c := range cases { 23 | newStr, err := runFixerOnString(TextareaFieldFixer{}, c.before) 24 | assert.NoError(t, err, c.description) 25 | assert.Equal(t, c.after, newStr, c.description) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /internal/verifier/admintwiglinter/fix_url_field_test.go: -------------------------------------------------------------------------------- 1 | package admintwiglinter 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestUrlFieldFixer(t *testing.T) { 10 | cases := []struct { 11 | description string 12 | before string 13 | after string 14 | }{ 15 | { 16 | description: "replace value with model-value", 17 | before: ``, 18 | after: ``, 19 | }, 20 | { 21 | description: "replace v-model:value with v-model", 22 | before: ``, 23 | after: ``, 24 | }, 25 | { 26 | description: "replace update:value event", 27 | before: ``, 28 | after: ``, 29 | }, 30 | { 31 | description: "process label slot", 32 | before: ``, 33 | after: ``, 34 | }, 35 | } 36 | 37 | for _, c := range cases { 38 | newStr, err := runFixerOnString(UrlFieldFixer{}, c.before) 39 | assert.NoError(t, err, c.description) 40 | assert.Equal(t, c.after, newStr, c.description) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /internal/verifier/admintwiglinter/fixer_popover.go: -------------------------------------------------------------------------------- 1 | package admintwiglinter 2 | 3 | import ( 4 | "github.com/shyim/go-version" 5 | 6 | "github.com/shopware/shopware-cli/internal/html" 7 | ) 8 | 9 | type PopoverFixer struct{} 10 | 11 | func init() { 12 | AddFixer(PopoverFixer{}) 13 | } 14 | 15 | func (p PopoverFixer) Check(node []html.Node) []CheckError { 16 | var checkErrors []CheckError 17 | 18 | html.TraverseNode(node, func(node *html.ElementNode) { 19 | if node.Tag == "sw-popover" { 20 | checkErrors = append(checkErrors, CheckError{ 21 | Message: "sw-popover is deprecated, use mt-floating-ui instead", 22 | Severity: "error", 23 | Identifier: "sw-popover", 24 | Line: node.Line, 25 | }) 26 | } 27 | }) 28 | 29 | return checkErrors 30 | } 31 | 32 | func (p PopoverFixer) Supports(version *version.Version) bool { 33 | return shopware67Constraint.Check(version) 34 | } 35 | 36 | func (p PopoverFixer) Fix(node []html.Node) error { 37 | html.TraverseNode(node, func(node *html.ElementNode) { 38 | if node.Tag == "sw-popover" { 39 | node.Tag = "mt-floating-ui" 40 | 41 | hasVIf := false 42 | var newAttrs html.NodeList 43 | 44 | for _, attrNode := range node.Attributes { 45 | // Check if the attribute is an html.Attribute 46 | if attr, ok := attrNode.(html.Attribute); ok { 47 | switch attr.Key { 48 | case "v-if": 49 | attr.Key = ":isOpened" 50 | newAttrs = append(newAttrs, attr) 51 | hasVIf = true 52 | case ":zIndex", ":resizeWidth": 53 | // Skip these attributes 54 | default: 55 | newAttrs = append(newAttrs, attr) 56 | } 57 | } else { 58 | // If it's not an html.Attribute (e.g., TwigIfNode), preserve it as is 59 | newAttrs = append(newAttrs, attrNode) 60 | } 61 | } 62 | 63 | if !hasVIf { 64 | newAttrs = append(newAttrs, html.Attribute{ 65 | Key: ":isOpened", 66 | Value: "true", 67 | }) 68 | } 69 | 70 | node.Attributes = newAttrs 71 | } 72 | }) 73 | 74 | return nil 75 | } 76 | -------------------------------------------------------------------------------- /internal/verifier/admintwiglinter/fixer_popover_test.go: -------------------------------------------------------------------------------- 1 | package admintwiglinter 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestPopover(t *testing.T) { 10 | cases := []struct { 11 | description string 12 | before string 13 | after string 14 | }{ 15 | { 16 | before: ``, 17 | after: ``, 18 | }, 19 | { 20 | before: ``, 21 | after: ``, 22 | }, 23 | { 24 | before: ``, 25 | after: ``, 26 | }, 27 | { 28 | before: ``, 29 | after: ``, 30 | }, 31 | } 32 | 33 | for _, c := range cases { 34 | newStr, err := runFixerOnString(PopoverFixer{}, c.before) 35 | assert.NoError(t, err, c.description) 36 | assert.Equal(t, c.after, newStr, c.description) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /internal/verifier/admintwiglinter/helper_test.go: -------------------------------------------------------------------------------- 1 | package admintwiglinter 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/shopware/shopware-cli/internal/html" 7 | ) 8 | 9 | func runFixerOnString(fixer AdminTwigFixer, content string) (string, error) { 10 | nodes, err := html.NewParser(content) 11 | if err != nil { 12 | return "", err 13 | } 14 | 15 | err = fixer.Fix(nodes) 16 | if err != nil { 17 | return "", err 18 | } 19 | 20 | var buf strings.Builder 21 | 22 | for _, node := range nodes { 23 | buf.WriteString(node.Dump(0)) 24 | } 25 | 26 | return buf.String(), nil 27 | } 28 | -------------------------------------------------------------------------------- /internal/verifier/admintwiglinter/root.go: -------------------------------------------------------------------------------- 1 | package admintwiglinter 2 | 3 | import ( 4 | "github.com/shyim/go-version" 5 | 6 | "github.com/shopware/shopware-cli/internal/html" 7 | ) 8 | 9 | var shopware67Constraint = version.MustConstraints(version.NewConstraint(">=6.7.0")) 10 | 11 | var availableFixers = []AdminTwigFixer{} 12 | 13 | func AddFixer(fixer AdminTwigFixer) { 14 | availableFixers = append(availableFixers, fixer) 15 | } 16 | 17 | type CheckError struct { 18 | Message string 19 | Severity string 20 | Identifier string 21 | Line int 22 | } 23 | 24 | func GetFixers(version *version.Version) []AdminTwigFixer { 25 | fixers := []AdminTwigFixer{} 26 | for _, fixer := range availableFixers { 27 | if fixer.Supports(version) { 28 | fixers = append(fixers, fixer) 29 | } 30 | } 31 | 32 | return fixers 33 | } 34 | 35 | type AdminTwigFixer interface { 36 | Check(node []html.Node) []CheckError 37 | Supports(version *version.Version) bool 38 | Fix(node []html.Node) error 39 | } 40 | -------------------------------------------------------------------------------- /internal/verifier/dir.go: -------------------------------------------------------------------------------- 1 | package verifier 2 | 3 | import ( 4 | "os" 5 | "path" 6 | ) 7 | 8 | var toolDirectory = "" 9 | 10 | func setToolDirectory(dir string) { 11 | toolDirectory = dir 12 | } 13 | 14 | func GetToolDirectory() string { 15 | if toolDirectory != "" { 16 | return toolDirectory 17 | } 18 | 19 | cwd, err := os.Getwd() 20 | if err != nil { 21 | return "" 22 | } 23 | 24 | toolDirectory = path.Join(cwd, "tools") 25 | 26 | return toolDirectory 27 | } 28 | -------------------------------------------------------------------------------- /internal/verifier/js/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /internal/verifier/js/configs/eslint.config.storefront.mjs: -------------------------------------------------------------------------------- 1 | import globals from "globals"; 2 | import pluginJs from "@eslint/js"; 3 | import tseslint from "typescript-eslint"; 4 | import eslintConfigPrettier from "eslint-config-prettier"; 5 | 6 | import storefrontRules from '@shopware-ag/storefront-eslint-rules'; 7 | 8 | /** @type {import('eslint').Linter.Config[]} */ 9 | export default [ 10 | {languageOptions: { globals: globals.browser }}, 11 | pluginJs.configs.recommended, 12 | ...tseslint.configs.recommended, 13 | storefrontRules, 14 | eslintConfigPrettier, 15 | { 16 | rules: { 17 | '@typescript-eslint/no-unused-vars': 'warn', 18 | '@typescript-eslint/no-unused-expressions': 'warn', 19 | '@typescript-eslint/no-this-alias': 'warn', 20 | '@typescript-eslint/no-require-imports': 'off', 21 | 'no-undef': 'off', 22 | 'no-alert': 'error', 23 | 'no-console': ['error', { allow: ['warn', 'error'] }], 24 | } 25 | } 26 | ]; -------------------------------------------------------------------------------- /internal/verifier/js/configs/prettierrc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import("prettier").Config} 3 | */ 4 | const config = { 5 | trailingComma: "es5", 6 | tabWidth: 4, 7 | semi: true, 8 | singleQuote: true, 9 | }; 10 | 11 | export default config; -------------------------------------------------------------------------------- /internal/verifier/js/configs/stylelint.config.administration.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('stylelint').Config} */ 2 | export default { 3 | extends: [ 4 | "stylelint-config-recommended-scss" 5 | ], 6 | customSyntax: "postcss-scss", 7 | plugins: [ 8 | "stylelint-scss", 9 | "@shopware-ag/admin-stylelint-rules" 10 | ], 11 | rules: { 12 | "selector-class-pattern": null, 13 | "import-notation": null, 14 | "declaration-property-value-no-unknown": null, 15 | "at-rule-no-unknown": null, 16 | "shopware-administration/no-scss-extension-import": true, 17 | "no-descending-specificity": null, 18 | "max-nesting-depth": [3, { 19 | "ignore": ["blockless-at-rules", "pseudo-classes"], 20 | "severity": "warning" 21 | }] 22 | } 23 | }; -------------------------------------------------------------------------------- /internal/verifier/js/configs/stylelint.config.storefront.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('stylelint').Config} */ 2 | export default { 3 | extends: [ 4 | "stylelint-config-recommended-scss" 5 | ], 6 | customSyntax: "postcss-scss", 7 | plugins: [ 8 | "stylelint-scss" 9 | ], 10 | rules: { 11 | "selector-class-pattern": null, 12 | "import-notation": null, 13 | "declaration-property-value-no-unknown": null, 14 | "at-rule-no-unknown": null, 15 | "no-descending-specificity": null, 16 | "max-nesting-depth": [3, { 17 | "ignore": ["blockless-at-rules", "pseudo-classes"], 18 | "severity": "warning" 19 | }], 20 | "selector-max-type": [0, { 21 | "message": "Selectors containing elements like \"%s\" should be avoided because the element type might change. Prefer to use .classes, #ids and [data-attributes] instead." 22 | }], 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /internal/verifier/js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "type": "module", 6 | "keywords": [], 7 | "author": "", 8 | "license": "ISC", 9 | "description": "", 10 | "devDependencies": { 11 | "@eslint/js": "^9.21.0", 12 | "@shopware-ag/storefront-eslint-rules": "file:packages/@shopware-ag/storefront-eslint-rules", 13 | "@shopware-ag/admin-eslint-rules": "file:packages/@shopware-ag/admin-eslint-rules", 14 | "@shopware-ag/admin-stylelint-rules": "file:packages/@shopware-ag/admin-stylelint-rules", 15 | "eslint": "^9.21.0", 16 | "eslint-config-prettier": "^10.0.1", 17 | "eslint-plugin-inclusive-language": "^2.2.1", 18 | "eslint-plugin-vue": "^10.0.0", 19 | "eslint-plugin-vuejs-accessibility": "^2.4.1", 20 | "globals": "^16.0.0", 21 | "prettier": "3.5.3", 22 | "typescript-eslint": "^8.24.1", 23 | "vue-eslint-parser": "^10.1.1", 24 | "stylelint": "^16.14.1", 25 | "stylelint-config-standard": "^38.0.0", 26 | "stylelint-config-recommended-scss": "^15.0.0", 27 | "stylelint-scss": "^6.11.1" 28 | } 29 | } -------------------------------------------------------------------------------- /internal/verifier/js/packages/@shopware-ag/admin-eslint-rules/index.js: -------------------------------------------------------------------------------- 1 | import { compareVersions } from 'compare-versions'; 2 | 3 | import noSnippetImport from "./no-snippet-import.js"; 4 | import noSrcImport from "./no-src-import.js"; 5 | import noVuex from './6.7/state-import.js'; 6 | import requireExplicitEmits from './6.7/require-explict-emits.js'; 7 | 8 | let rules = { 9 | "no-src-import": noSrcImport, 10 | "no-snippet-import": noSnippetImport, 11 | "no-shopware-store": noVuex, 12 | "require-explict-emits": requireExplicitEmits, 13 | } 14 | 15 | if (process.env.SHOPWARE_VERSION) { 16 | rules = Object.fromEntries( 17 | Object.entries(rules).filter(([_, rule]) => { 18 | if (!rule.meta?.minShopwareVersion) { 19 | return true; 20 | } 21 | 22 | return compareVersions(process.env.SHOPWARE_VERSION, rule.meta.minShopwareVersion) >= 0; 23 | }) 24 | ); 25 | } 26 | 27 | const config = { 28 | plugins: { 29 | "shopware-admin": { 30 | rules: rules, 31 | } 32 | }, 33 | rules: {} 34 | }; 35 | 36 | Object.keys(rules).forEach(ruleName => { 37 | config.rules[`shopware-admin/${ruleName}`] = 'error'; 38 | }); 39 | 40 | export default config; -------------------------------------------------------------------------------- /internal/verifier/js/packages/@shopware-ag/admin-eslint-rules/no-src-import.js: -------------------------------------------------------------------------------- 1 | export default { 2 | create(context) { 3 | return { 4 | ImportDeclaration(node) { 5 | const invalidNodeSources = []; 6 | invalidNodeSources.push(node.source.value.startsWith('@administration/')); 7 | 8 | if (invalidNodeSources.includes(true)) { 9 | context.report({ 10 | loc: node.source.loc.start, 11 | message: `\ 12 | You can't use imports directly from the Shopware Core via "${node.source.value}". \ 13 | Use the global Shopware object directly instead (https://developer.shopware.com/docs/guides/plugins/plugins/administration/the-shopware-object)`, 14 | }); 15 | } 16 | }, 17 | }; 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /internal/verifier/js/packages/@shopware-ag/admin-eslint-rules/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@shopware-ag/admin-eslint-rules", 3 | "type": "module", 4 | "description": "Shopware Administration ESLint rules", 5 | "version": "1.0.0", 6 | "main": "index.js", 7 | "dependencies": { 8 | "compare-versions": "^6.1" 9 | }, 10 | "peerDependencies": { 11 | "eslint": "^9" 12 | }, 13 | "exports": { 14 | ".": { 15 | "import": "./index.js", 16 | "require": "./index.js" 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /internal/verifier/js/packages/@shopware-ag/admin-stylelint-rules/index.js: -------------------------------------------------------------------------------- 1 | import wrongScssImport from "./wrong-scss-import.js"; 2 | 3 | export default [wrongScssImport] -------------------------------------------------------------------------------- /internal/verifier/js/packages/@shopware-ag/admin-stylelint-rules/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@shopware-ag/admin-stylelint-rules", 3 | "version": "0.0.1", 4 | "main": "index.js", 5 | "type": "module", 6 | "exports": { 7 | ".": { 8 | "import": "./index.js", 9 | "require": "./index.js" 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /internal/verifier/js/packages/@shopware-ag/admin-stylelint-rules/wrong-scss-import.js: -------------------------------------------------------------------------------- 1 | import stylelint from "stylelint"; 2 | 3 | export const ruleName = "shopware-administration/no-scss-extension-import"; 4 | 5 | export const messages = stylelint.utils.ruleMessages(ruleName, { 6 | rejected: 7 | 'Avoid using the ".scss" extension in imports that start with "~scss". ' + 8 | 'Use "@import \'~scss/variables\'" instead.' 9 | }); 10 | 11 | export default stylelint.createPlugin( 12 | ruleName, 13 | (primaryOption, secondaryOptions, context) => { 14 | return (root, result) => { 15 | const validOptions = stylelint.utils.validateOptions(result, ruleName, { 16 | actual: primaryOption, 17 | }); 18 | if (!validOptions) { 19 | return; 20 | } 21 | 22 | root.walkAtRules("import", (atRule) => { 23 | // Get import params; remove quotes and semicolon elements. 24 | let importPath = atRule.params.trim(); 25 | 26 | // Remove wrapping quotes (either single or double) 27 | const matchQuotes = importPath.match(/^(['"])(.*)\1$/); 28 | if (matchQuotes) { 29 | importPath = matchQuotes[2]; 30 | } 31 | 32 | // Check if it starts with "~scss" and ends with ".scss" 33 | if (importPath.startsWith("~scss") && importPath.endsWith(".scss")) { 34 | // Create the fixed import path by stripping the .scss extension. 35 | const fixedPath = importPath.replace(/\.scss$/, ""); 36 | 37 | if (context.fix) { 38 | // Use the same quote character or default to double quotes. 39 | const quote = matchQuotes ? matchQuotes[1] : '"'; 40 | atRule.params = `${quote}${fixedPath}${quote}`; 41 | } else { 42 | stylelint.utils.report({ 43 | message: messages.rejected, 44 | node: atRule, 45 | result, 46 | ruleName, 47 | }); 48 | } 49 | } 50 | }); 51 | }; 52 | } 53 | ); 54 | 55 | -------------------------------------------------------------------------------- /internal/verifier/js/packages/@shopware-ag/storefront-eslint-rules/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2024 shopware AG 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 4 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation the 5 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit 6 | persons to whom the Software is furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all 9 | copies or substantial portions of the Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO 12 | THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 13 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 14 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /internal/verifier/js/packages/@shopware-ag/storefront-eslint-rules/README.md: -------------------------------------------------------------------------------- 1 | # @shopware-ag/storefront-eslint-rules 2 | 3 | This package provides custom ESLint rules for Shopware Storefront projects. These rules help to ensure code quality, promote best practices, and assist in migrating from older code patterns to more modern approaches. 4 | 5 | ## Installation 6 | 7 | ``` 8 | npm install @shopware-ag/storefront-eslint-rules --save-dev 9 | ``` 10 | 11 | ## Usage 12 | 13 | Add the following to your ESLint configuration file (e.g., .eslintrc.js): 14 | 15 | ```diff 16 | +import storefrontRules from '@shopware-ag/storefront-eslint-rules'; 17 | 18 | /** @type {import('eslint').Linter.Config[]} */ 19 | export default [ 20 | {languageOptions: { globals: globals.browser }}, 21 | pluginJs.configs.recommended, 22 | ...tseslint.configs.recommended, 23 | + storefrontRules, 24 | { 25 | rules: { 26 | '@typescript-eslint/no-unused-vars': 'warn', 27 | '@typescript-eslint/no-unused-expressions': 'warn', 28 | '@typescript-eslint/no-this-alias': 'warn', 29 | '@typescript-eslint/no-require-imports': 'off', 30 | 'no-undef': 'off', 31 | 'no-alert': 'error', 32 | 'no-console': ['error', { allow: ['warn', 'error'] }], 33 | } 34 | } 35 | ]; 36 | ``` 37 | 38 | ## Rules 39 | 40 | ### migrate-plugin-manager 41 | 42 | This rule flags imports from src/plugin-system/plugin.manager and suggests using window.PluginManager instead. It also flags imports from src/plugin-system/plugin.class and suggests using window.PluginBaseClass. 43 | 44 | ### no-dom-access-helper 45 | 46 | This rule identifies usages of the DomAccessHelper and suggests using native DOM methods instead. 47 | 48 | ### no-http-client 49 | 50 | This rule identifies usages of the HttpClient and suggests using the fetch API instead. 51 | 52 | ### no-query-string 53 | 54 | This rule identifies usages of the query-string library and suggests using URLSearchParams instead. 55 | 56 | ## Contributing 57 | 58 | Contributions are welcome! Please read the contributing guidelines before submitting a pull request. 59 | 60 | ## License 61 | 62 | MIT 63 | -------------------------------------------------------------------------------- /internal/verifier/js/packages/@shopware-ag/storefront-eslint-rules/index.js: -------------------------------------------------------------------------------- 1 | import MigratePluginManager from './plugin-manager.js'; 2 | import DomAccessHelper from "./dom-access-helper.js"; 3 | import HttpClient from "./http-client.js"; 4 | import QueryString from "./query-string.js"; 5 | 6 | export default { 7 | plugins: { 8 | "shopware-storefront": { 9 | rules: { 10 | "migrate-plugin-manager": MigratePluginManager, 11 | "no-dom-access-helper": DomAccessHelper, 12 | "no-http-client": HttpClient, 13 | 'no-query-string': QueryString, 14 | }, 15 | } 16 | }, 17 | rules: { 18 | 'shopware-storefront/migrate-plugin-manager': 'error', 19 | 'shopware-storefront/no-dom-access-helper': 'error', 20 | 'shopware-storefront/no-http-client': 'error', 21 | 'shopware-storefront/no-query-string': 'error', 22 | } 23 | } -------------------------------------------------------------------------------- /internal/verifier/js/packages/@shopware-ag/storefront-eslint-rules/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@shopware-ag/storefront-eslint-rules", 3 | "type": "module", 4 | "description": "Shopware Storefront ESLint rules", 5 | "version": "1.0.0", 6 | "main": "index.js", 7 | "peerDependencies": { 8 | "eslint": "^9" 9 | }, 10 | "exports": { 11 | ".": { 12 | "import": "./index.js", 13 | "require": "./index.js" 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /internal/verifier/js/packages/@shopware-ag/storefront-eslint-rules/plugin-manager.js: -------------------------------------------------------------------------------- 1 | export default { 2 | meta: { 3 | type: "suggestion", 4 | docs: { 5 | description: "Migrate PluginManager import to window.PluginManager assignment", 6 | category: "Migration", 7 | recommended: false 8 | }, 9 | fixable: "code" 10 | }, 11 | 12 | create(context) { 13 | return { 14 | ImportDeclaration(node) { 15 | // Check if the import is from 'src/plugin-system/plugin.manager' 16 | if (node.source.value === "src/plugin-system/plugin.manager") { 17 | // Get the imported variable name 18 | const importedName = node.specifiers[0]?.local?.name; 19 | 20 | if (importedName) { 21 | context.report({ 22 | node, 23 | message: `Import from plugin.manager should use window.PluginManager`, 24 | fix(fixer) { 25 | return fixer.replaceText( 26 | node, 27 | `const ${importedName} = window.PluginManager;` 28 | ); 29 | }, 30 | }); 31 | } 32 | } 33 | 34 | if (node.source.value === 'src/plugin-system/plugin.class') { 35 | // Get the imported variable name 36 | const importedName = node.specifiers[0]?.local?.name; 37 | 38 | if (importedName) { 39 | context.report({ 40 | node, 41 | message: `Import from src/plugin-system/plugin.class should use window.PluginBaseClass`, 42 | fix(fixer) { 43 | return fixer.replaceText( 44 | node, 45 | `const ${importedName} = window.PluginBaseClass;` 46 | ); 47 | }, 48 | }); 49 | } 50 | } 51 | }, 52 | }; 53 | }, 54 | }; -------------------------------------------------------------------------------- /internal/verifier/php/.gitignore: -------------------------------------------------------------------------------- 1 | vendor -------------------------------------------------------------------------------- /internal/verifier/php/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": { 3 | "friendsofphp/php-cs-fixer": "^3.68", 4 | "phpstan/phpstan": "^2", 5 | "shopwarelabs/phpstan-shopware": "*", 6 | "phpstan/extension-installer": "*", 7 | "phpstan/phpstan-symfony": "*", 8 | "rector/rector": "^2.0", 9 | "frosh/shopware-rector": "^0.5.0", 10 | "phpstan/phpstan-deprecation-rules": "^2.0", 11 | "spaze/phpstan-disallowed-calls": "^4.0" 12 | }, 13 | "replace": { 14 | "symfony/polyfill-ctype": "*", 15 | "symfony/polyfill-mbstring": "*", 16 | "symfony/polyfill-intl-normalizer": "*", 17 | "symfony/polyfill-intl-grapheme": "*", 18 | "symfony/polyfill-php80": "*", 19 | "symfony/polyfill-php81": "*" 20 | }, 21 | "config": { 22 | "classmap-authoritative": true, 23 | "allow-plugins": { 24 | "phpstan/extension-installer": true 25 | }, 26 | "platform": { 27 | "php": "8.2.28" 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /internal/verifier/php/configs/phpstan.neon: -------------------------------------------------------------------------------- 1 | includes: 2 | - %env.PHP_DIR%/vendor/spaze/phpstan-disallowed-calls/disallowed-dangerous-calls.neon 3 | - %env.PHP_DIR%/vendor/spaze/phpstan-disallowed-calls/disallowed-execution-calls.neon 4 | 5 | parameters: 6 | level: 5 7 | 8 | bootstrapFiles: 9 | - %currentWorkingDirectory%/vendor/autoload.php 10 | paths: 11 | - %currentWorkingDirectory% 12 | excludePaths: 13 | - vendor (?) 14 | - vendor-bin (?) 15 | - tests/ (?) 16 | - Test/ (?) 17 | - autoload-dist/vendor (?) 18 | 19 | reportUnmatchedIgnoredErrors: false 20 | tipsOfTheDay: false 21 | disallowedFunctionCalls: 22 | - 23 | function: 'dd()' 24 | message: 'do not use dd() in production code' 25 | - 26 | function: 'dump()' 27 | message: 'do not use dump() in production code' 28 | - 29 | function: 'session_write_close()' 30 | message: 'use save method of the SessionInterface instead of session_write_close(). E.g. $request->getSession()->save()' 31 | 32 | -------------------------------------------------------------------------------- /internal/verifier/phpcsfixer.go: -------------------------------------------------------------------------------- 1 | package verifier 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "os/exec" 7 | "path" 8 | 9 | "golang.org/x/sync/errgroup" 10 | ) 11 | 12 | type PHPCSFixer struct{} 13 | 14 | func (p PHPCSFixer) Name() string { 15 | return "php-cs-fixer" 16 | } 17 | 18 | func (p PHPCSFixer) Check(ctx context.Context, check *Check, config ToolConfig) error { 19 | return nil 20 | } 21 | 22 | func (p PHPCSFixer) Fix(ctx context.Context, config ToolConfig) error { 23 | return nil 24 | } 25 | 26 | func (p PHPCSFixer) getConfigPath(toolDirectory, rootDir string) string { 27 | if _, err := os.Stat(path.Join(rootDir, ".php-cs-fixer.dist.php")); err == nil { 28 | return path.Join(rootDir, ".php-cs-fixer.dist.php") 29 | } 30 | 31 | return path.Join(toolDirectory, "php", "configs", "php-cs-fixer.dist.php") 32 | } 33 | 34 | func (p PHPCSFixer) Format(ctx context.Context, config ToolConfig, dryRun bool) error { 35 | // Apps don't have an composer.json file, skip them 36 | if _, err := os.Stat(path.Join(config.RootDir, "composer.json")); err != nil { 37 | //nolint: nilerr 38 | return nil 39 | } 40 | 41 | var gr errgroup.Group 42 | 43 | for _, sourceDirectory := range config.SourceDirectories { 44 | fixDir := sourceDirectory 45 | 46 | args := []string{"fix", "--config", p.getConfigPath(config.ToolDirectory, config.RootDir), fixDir} 47 | if dryRun { 48 | args = append(args, "--dry-run") 49 | } 50 | 51 | cmd := exec.CommandContext(ctx, path.Join(config.ToolDirectory, "php", "vendor", "bin", "php-cs-fixer"), args...) 52 | cmd.Env = append(os.Environ(), "PHP_CS_FIXER_IGNORE_ENV=1") 53 | cmd.Dir = config.RootDir 54 | cmd.Stdout = os.Stdout 55 | cmd.Stderr = os.Stderr 56 | 57 | gr.Go(func() error { 58 | return cmd.Run() 59 | }) 60 | } 61 | 62 | return gr.Wait() 63 | } 64 | 65 | func init() { 66 | AddTool(PHPCSFixer{}) 67 | } 68 | -------------------------------------------------------------------------------- /internal/verifier/phpstan_test.go: -------------------------------------------------------------------------------- 1 | package verifier 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestPhpStan_isUselessDeprecation(t *testing.T) { 10 | tests := []struct { 11 | name string 12 | message string 13 | want bool 14 | }{ 15 | { 16 | name: "message without tag version", 17 | message: "Some deprecated method without version tag", 18 | want: true, 19 | }, 20 | { 21 | name: "message with tag version", 22 | message: "Method deprecated since tag:v6.5.0", 23 | want: false, 24 | }, 25 | { 26 | name: "parameter removal message with tag", 27 | message: "Parameter $foo will be removed in tag:v6.6.0", 28 | want: true, 29 | }, 30 | { 31 | name: "parameter removal message without tag", 32 | message: "Parameter $bar will be removed", 33 | want: true, 34 | }, 35 | { 36 | name: "return type change reason with tag", 37 | message: "Deprecated method tag:v6.5.0 reason:return-type-change", 38 | want: true, 39 | }, 40 | { 41 | name: "new optional parameter reason with tag", 42 | message: "Deprecated constructor tag:v6.5.0 reason:new-optional-parameter", 43 | want: true, 44 | }, 45 | { 46 | name: "valid deprecation with tag", 47 | message: "Method Foo::bar() is deprecated since tag:v6.5.0 and will be removed", 48 | want: false, 49 | }, 50 | { 51 | name: "multiple version tags", 52 | message: "Deprecated since tag:v6.4.0, updated in tag:v6.5.0", 53 | want: false, 54 | }, 55 | { 56 | name: "invalid version tag format", 57 | message: "Method deprecated since tag:invalid-version", 58 | want: true, 59 | }, 60 | } 61 | 62 | p := PhpStan{} 63 | for _, tt := range tests { 64 | t.Run(tt.name, func(t *testing.T) { 65 | got := p.isUselessDeprecation(tt.message) 66 | assert.Equal(t, tt.want, got) 67 | }) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /internal/verifier/prettier.go: -------------------------------------------------------------------------------- 1 | package verifier 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "os/exec" 7 | "path" 8 | 9 | "golang.org/x/sync/errgroup" 10 | ) 11 | 12 | var ignoredPaths = ` 13 | package-lock.json 14 | Resources/public/** 15 | dist/** 16 | Resources/store/** 17 | ` 18 | 19 | type Prettier struct{} 20 | 21 | func (b Prettier) Name() string { 22 | return "prettier" 23 | } 24 | 25 | func (b Prettier) Check(ctx context.Context, check *Check, config ToolConfig) error { 26 | return nil 27 | } 28 | 29 | func (b Prettier) Fix(ctx context.Context, config ToolConfig) error { 30 | return nil 31 | } 32 | 33 | func (b Prettier) Format(ctx context.Context, config ToolConfig, dryRun bool) error { 34 | var gr errgroup.Group 35 | 36 | for _, sourceDirectory := range config.SourceDirectories { 37 | sourceDirectory := sourceDirectory 38 | 39 | if err := os.WriteFile(path.Join(sourceDirectory, ".prettierignore"), []byte(ignoredPaths), 0o644); err != nil { 40 | return err 41 | } 42 | 43 | args := []string{ 44 | path.Join(config.ToolDirectory, "js", "node_modules", ".bin", "prettier"), 45 | "--config", 46 | path.Join(config.ToolDirectory, "js", "configs", "prettierrc.js"), 47 | ".", 48 | } 49 | 50 | if !dryRun { 51 | args = append(args, "--write") 52 | } 53 | 54 | gr.Go(func() error { 55 | cmd := exec.CommandContext(ctx, "node", args...) 56 | cmd.Dir = sourceDirectory 57 | cmd.Stderr = os.Stderr 58 | cmd.Stdout = os.Stdout 59 | 60 | if err := cmd.Run(); err != nil { 61 | return err 62 | } 63 | 64 | return os.Remove(path.Join(sourceDirectory, ".prettierignore")) 65 | }) 66 | } 67 | 68 | return gr.Wait() 69 | } 70 | 71 | func init() { 72 | AddTool(Prettier{}) 73 | } 74 | -------------------------------------------------------------------------------- /internal/verifier/result.go: -------------------------------------------------------------------------------- 1 | package verifier 2 | 3 | import ( 4 | "strings" 5 | "sync" 6 | ) 7 | 8 | type Check struct { 9 | Results []CheckResult `json:"results"` 10 | mutex sync.Mutex 11 | } 12 | 13 | func NewCheck() *Check { 14 | return &Check{ 15 | Results: []CheckResult{}, 16 | } 17 | } 18 | 19 | func (c *Check) AddResult(result CheckResult) { 20 | c.mutex.Lock() 21 | defer c.mutex.Unlock() 22 | c.Results = append(c.Results, result) 23 | } 24 | 25 | func (c *Check) HasErrors() bool { 26 | for _, r := range c.Results { 27 | if r.Severity == "error" { 28 | return true 29 | } 30 | } 31 | 32 | return false 33 | } 34 | 35 | func (c *Check) RemoveByIdentifier(ignores []ToolConfigIgnore) *Check { 36 | c.mutex.Lock() 37 | defer c.mutex.Unlock() 38 | 39 | filtered := make([]CheckResult, 0) 40 | for _, r := range c.Results { 41 | shouldKeep := true 42 | for _, ignore := range ignores { 43 | // Only ignore all matches when identifier is the only field specified 44 | if ignore.Identifier != "" && ignore.Path == "" && ignore.Message == "" { 45 | if r.Identifier == ignore.Identifier { 46 | shouldKeep = false 47 | break 48 | } 49 | } 50 | 51 | // If path is specified with identifier (but no message), match both 52 | if ignore.Identifier != "" && ignore.Path != "" && ignore.Message == "" { 53 | if r.Identifier == ignore.Identifier && r.Path == ignore.Path { 54 | shouldKeep = false 55 | break 56 | } 57 | } 58 | 59 | // Handle message-based ignores (when no identifier is specified) 60 | if ignore.Identifier == "" && ignore.Message != "" && strings.Contains(r.Message, ignore.Message) && (r.Path == ignore.Path || ignore.Path == "") { 61 | shouldKeep = false 62 | break 63 | } 64 | } 65 | if shouldKeep { 66 | filtered = append(filtered, r) 67 | } 68 | } 69 | c.Results = filtered 70 | 71 | return c 72 | } 73 | 74 | type CheckResult struct { 75 | // The path to the file that was checked 76 | Path string `json:"path"` 77 | // The line number of the issue 78 | Line int `json:"line"` 79 | Message string `json:"message"` 80 | // The severity of the issue 81 | Severity string `json:"severity"` 82 | 83 | Identifier string `json:"identifier"` 84 | } 85 | 86 | const ( 87 | CheckSeverityError = "error" 88 | CheckSeverityWarn = "warning" 89 | ) 90 | -------------------------------------------------------------------------------- /internal/verifier/sw_cli.go: -------------------------------------------------------------------------------- 1 | package verifier 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/shopware/shopware-cli/extension" 7 | ) 8 | 9 | type SWCLI struct{} 10 | 11 | func (s SWCLI) Name() string { 12 | return "sw-cli" 13 | } 14 | 15 | func (s SWCLI) Check(ctx context.Context, check *Check, config ToolConfig) error { 16 | if config.Extension == nil { 17 | return nil 18 | } 19 | 20 | validationContext := extension.RunValidation(ctx, config.Extension) 21 | 22 | if config.InputWasDirectory { 23 | validationContext.ApplyIgnores([]extension.ConfigValidationIgnoreItem{ 24 | { 25 | Identifier: "zip.disallowed_file", 26 | Message: ".gitignore is not allowed in the zip file", 27 | }, 28 | }) 29 | } 30 | 31 | for _, err := range validationContext.Errors() { 32 | check.AddResult(CheckResult{ 33 | Path: "", 34 | Line: 0, 35 | Message: err.Message, 36 | Identifier: err.Identifier, 37 | Severity: "error", 38 | }) 39 | } 40 | 41 | for _, err := range validationContext.Warnings() { 42 | check.AddResult(CheckResult{ 43 | Path: "", 44 | Line: 0, 45 | Message: err.Message, 46 | Identifier: err.Identifier, 47 | Severity: "warning", 48 | }) 49 | } 50 | 51 | return nil 52 | } 53 | 54 | func (s SWCLI) Fix(ctx context.Context, config ToolConfig) error { 55 | return nil 56 | } 57 | 58 | func (s SWCLI) Format(ctx context.Context, config ToolConfig, dryRun bool) error { 59 | return nil 60 | } 61 | 62 | func init() { 63 | AddTool(SWCLI{}) 64 | } 65 | -------------------------------------------------------------------------------- /logging/logger.go: -------------------------------------------------------------------------------- 1 | package logging 2 | 3 | import ( 4 | "context" 5 | 6 | "go.uber.org/zap" 7 | "go.uber.org/zap/zapcore" 8 | ) 9 | 10 | // contextKey is a private string type to prevent collisions in the context map. 11 | type contextKey string 12 | 13 | // loggerKey points to the value in the context where the logging is stored. 14 | const loggerKey = contextKey("logging") 15 | 16 | var fallbackLogger *zap.SugaredLogger 17 | 18 | func NewLogger(verbose bool) *zap.SugaredLogger { 19 | loggerCfg := zap.NewDevelopmentConfig() 20 | loggerCfg.EncoderConfig.MessageKey = "message" 21 | loggerCfg.EncoderConfig.TimeKey = "timestamp" 22 | loggerCfg.EncoderConfig.EncodeDuration = zapcore.NanosDurationEncoder 23 | loggerCfg.EncoderConfig.StacktraceKey = "error.stack" 24 | loggerCfg.EncoderConfig.FunctionKey = "logging.method_name" 25 | loggerCfg.DisableStacktrace = true 26 | loggerCfg.DisableCaller = true 27 | loggerCfg.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder 28 | 29 | if !verbose { 30 | loggerCfg.Level = zap.NewAtomicLevelAt(zap.InfoLevel) 31 | loggerCfg.EncoderConfig.TimeKey = "" 32 | } 33 | 34 | logger, err := loggerCfg.Build() 35 | if err != nil { 36 | logger = zap.NewNop() 37 | } 38 | 39 | return logger.Sugar() 40 | } 41 | 42 | func WithLogger(ctx context.Context, logger *zap.SugaredLogger) context.Context { 43 | return context.WithValue(ctx, loggerKey, logger) 44 | } 45 | 46 | func FromContext(ctx context.Context) *zap.SugaredLogger { 47 | if logger, ok := ctx.Value(loggerKey).(*zap.SugaredLogger); ok { 48 | return logger 49 | } 50 | 51 | if fallbackLogger == nil { 52 | loggerCfg := zap.NewProductionConfig() 53 | logger, _ := loggerCfg.Build() 54 | 55 | fallbackLogger = logger.Sugar() 56 | } 57 | 58 | return fallbackLogger 59 | } 60 | 61 | func DisableLogger(ctx context.Context) context.Context { 62 | return WithLogger(ctx, zap.NewNop().Sugar()) 63 | } 64 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/shopware/shopware-cli/cmd" 7 | ) 8 | 9 | func main() { 10 | cmd.Execute(context.Background()) 11 | } 12 | -------------------------------------------------------------------------------- /scripts/completion.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | rm -rf completions 4 | mkdir completions 5 | go run . completion bash > completions/shopware-cli.bash 6 | go run . completion zsh > completions/shopware-cli.zsh 7 | go run . completion fish > completions/shopware-cli.fish -------------------------------------------------------------------------------- /scripts/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | set -euo pipefail 3 | 4 | exec "$@" 5 | -------------------------------------------------------------------------------- /shop/client.go: -------------------------------------------------------------------------------- 1 | package shop 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "fmt" 7 | "net/http" 8 | "os" 9 | 10 | adminSdk "github.com/friendsofshopware/go-shopware-admin-api-sdk" 11 | ) 12 | 13 | func newShopCredentials(config *Config) (adminSdk.OAuthCredentials, error) { 14 | clientId, clientSecret := os.Getenv("SHOPWARE_CLI_API_CLIENT_ID"), os.Getenv("SHOPWARE_CLI_API_CLIENT_SECRET") 15 | 16 | if clientId != "" && clientSecret != "" { 17 | return adminSdk.NewIntegrationCredentials(clientId, clientSecret, []string{"write"}), nil 18 | } 19 | 20 | username, password := os.Getenv("SHOPWARE_CLI_API_USERNAME"), os.Getenv("SHOPWARE_CLI_API_PASSWORD") 21 | 22 | if username != "" && password != "" { 23 | return adminSdk.NewPasswordCredentials(username, password, []string{"write"}), nil 24 | } 25 | 26 | if config.AdminApi == nil { 27 | return nil, fmt.Errorf("admin-api is not enabled in config") 28 | } 29 | 30 | if config.AdminApi.Username != "" { 31 | return adminSdk.NewPasswordCredentials(config.AdminApi.Username, config.AdminApi.Password, []string{"write"}), nil 32 | } 33 | 34 | return adminSdk.NewIntegrationCredentials(config.AdminApi.ClientId, config.AdminApi.ClientSecret, []string{"write"}), nil 35 | } 36 | 37 | func NewShopClient(ctx context.Context, config *Config) (*adminSdk.Client, error) { 38 | skipSSLCert := false 39 | 40 | if config.AdminApi != nil { 41 | skipSSLCert = config.AdminApi.DisableSSLCheck 42 | } 43 | 44 | if os.Getenv("SHOPWARE_CLI_API_DISABLE_SSL_CHECK") == "true" { 45 | skipSSLCert = true 46 | } 47 | 48 | tr := &http.Transport{ 49 | TLSClientConfig: &tls.Config{ 50 | MinVersion: tls.VersionTLS12, 51 | InsecureSkipVerify: skipSSLCert, // nolint:gosec 52 | }, 53 | } 54 | client := &http.Client{Transport: tr} 55 | 56 | shopUrl := os.Getenv("SHOPWARE_CLI_API_URL") 57 | 58 | if shopUrl == "" { 59 | shopUrl = config.URL 60 | } 61 | 62 | creds, err := newShopCredentials(config) 63 | if err != nil { 64 | return nil, fmt.Errorf("newShopCredentials: %v", err) 65 | } 66 | 67 | return adminSdk.NewApiClient(ctx, shopUrl, creds, client) 68 | } 69 | -------------------------------------------------------------------------------- /shop/config_test.go: -------------------------------------------------------------------------------- 1 | package shop 2 | 3 | import ( 4 | "os" 5 | "path" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestConfigMerging(t *testing.T) { 12 | tmpDir := t.TempDir() 13 | 14 | t.Chdir(tmpDir) 15 | 16 | baseConfig := []byte(` 17 | admin_api: 18 | client_id: ${SHOPWARE_CLI_CLIENT_ID} 19 | client_secret: ${SHOPWARE_CLI_CLIENT_SECRET} 20 | dump: 21 | where: 22 | customer: "email LIKE '%@nuonic.de' OR email LIKE '%@xyz.com'" 23 | nodata: 24 | - promotion 25 | `) 26 | 27 | stagingConfig := []byte(` 28 | url: https://xyz.nuonic.dev 29 | include: 30 | - base.yml 31 | sync: 32 | config: 33 | - settings: 34 | core.store.licenseHost: xyz.nuonic.dev 35 | `) 36 | 37 | baseFilePath := path.Join(tmpDir, "base.yml") 38 | stagingFilePath := path.Join(tmpDir, "staging.yml") 39 | 40 | assert.NoError(t, os.WriteFile(baseFilePath, baseConfig, 0644)) 41 | assert.NoError(t, os.WriteFile(stagingFilePath, stagingConfig, 0644)) 42 | 43 | config, err := ReadConfig(stagingFilePath, false) 44 | assert.NoError(t, err) 45 | 46 | assert.NotNil(t, config.Sync) 47 | assert.NotNil(t, config.Sync.Config) 48 | assert.Len(t, config.Sync.Config, 1) 49 | assert.Equal(t, "xyz.nuonic.dev", config.Sync.Config[0].Settings["core.store.licenseHost"]) 50 | 51 | assert.NoError(t, os.RemoveAll(tmpDir)) 52 | } 53 | -------------------------------------------------------------------------------- /shop/console.go: -------------------------------------------------------------------------------- 1 | package shop 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "os" 8 | "path" 9 | 10 | "github.com/shopware/shopware-cli/internal/phpexec" 11 | ) 12 | 13 | type ConsoleResponse struct { 14 | Commands []struct { 15 | Name string `json:"name"` 16 | Hidden bool `json:"hidden"` 17 | Definition struct { 18 | Arguments interface{} `json:"arguments"` 19 | Options map[string]struct { 20 | Shortcut string `json:"shortcut"` 21 | } `json:"options"` 22 | } `json:"definition"` 23 | } `json:"commands"` 24 | } 25 | 26 | func (c ConsoleResponse) GetCommandOptions(name string) []string { 27 | for _, command := range c.Commands { 28 | if !command.Hidden && command.Name == name { 29 | options := make([]string, 0) 30 | for optionName := range command.Definition.Options { 31 | options = append(options, fmt.Sprintf("--%s", optionName)) 32 | } 33 | 34 | return options 35 | } 36 | } 37 | return nil 38 | } 39 | 40 | func GetConsoleCompletion(ctx context.Context, projectRoot string) (*ConsoleResponse, error) { 41 | cachePath := path.Join(projectRoot, "var", "cache", "console_commands.json") 42 | 43 | if _, err := os.Stat(cachePath); err == nil { 44 | var resp ConsoleResponse 45 | 46 | bytes, err := os.ReadFile(cachePath) 47 | if err != nil { 48 | return nil, err 49 | } 50 | 51 | if err := json.Unmarshal(bytes, &resp); err != nil { 52 | return nil, err 53 | } 54 | 55 | return &resp, nil 56 | } 57 | 58 | consoleCommand := phpexec.ConsoleCommand(ctx, "list", "--format=json") 59 | consoleCommand.Dir = projectRoot 60 | 61 | commandJson, err := consoleCommand.Output() 62 | if err != nil { 63 | return nil, err 64 | } 65 | 66 | var resp ConsoleResponse 67 | 68 | if err := json.Unmarshal(commandJson, &resp); err != nil { 69 | return nil, err 70 | } 71 | 72 | if err := os.WriteFile(cachePath, commandJson, os.ModePerm); err != nil { 73 | return nil, err 74 | } 75 | 76 | return &resp, nil 77 | } 78 | --------------------------------------------------------------------------------