├── .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 | [](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 | Foo
2 | -----
3 |
4 |
5 | Foo
6 |
7 |
--------------------------------------------------------------------------------
/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 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/internal/html/testdata/12-multi-line-between-only-elements.txt:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | -----
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/internal/html/testdata/13-long-attribute-is-on-new-line.txt:
--------------------------------------------------------------------------------
1 |
2 | -----
3 |
--------------------------------------------------------------------------------
/internal/html/testdata/14-html-element-with-content.txt:
--------------------------------------------------------------------------------
1 | {{ item.mainPseudovariant.product.translated.name }}
2 | -----
3 |
4 |
5 | {{ item.mainPseudovariant.product.translated.name }}
6 |
7 |
--------------------------------------------------------------------------------
/internal/html/testdata/15-multiple-template-elements.txt:
--------------------------------------------------------------------------------
1 | Template 1
Template 2
2 | -----
3 |
4 | Template 1
5 |
6 |
7 |
8 | Template 2
9 |
--------------------------------------------------------------------------------
/internal/html/testdata/16-multiple-template-elements-with-root.txt:
--------------------------------------------------------------------------------
1 | Template 1
Template 2
2 | -----
3 |
4 |
5 | Template 1
6 |
7 |
8 |
9 | Template 2
10 |
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 |
6 | -----
7 |
--------------------------------------------------------------------------------
/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 |
12 | {% endblock %}
13 | -----
14 | {% block sw_cms_block_product_listing_preview %}
15 |
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: `My Label`,
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: `My Label`,
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 |
59 | My Label
60 |
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: `My Label`,
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: `My Label`,
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: `
38 | Foobar
39 | `,
40 | after: ``,
41 | },
42 | {
43 | description: "remove hint slot and add comment node",
44 | before: `
45 | Foobar
46 | `,
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: `My Label`,
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: `FOO`,
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: `My Label`,
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 |
--------------------------------------------------------------------------------