├── .cspell.json ├── .editorconfig ├── .eslintrc.js ├── .github ├── CODEOWNERS └── workflows │ └── ci.yml ├── .gitignore ├── .npmignore ├── .scs-commander.dist ├── CHANGELOG.md ├── LICENSE ├── README.md ├── index.js ├── lib ├── changelogMarkdownParser.js ├── cliStatusIndicator.js ├── client │ ├── retryInterceptor.js │ ├── shopwareAuthenticator.js │ ├── shopwareStoreClient.js │ └── spinnerInterceptors.js ├── commands │ ├── changelog.js │ ├── compatibility.js │ ├── description.js │ ├── dumpPlugin.js │ ├── list.js │ └── upload.js ├── consoleLogger.js ├── envLoader.js ├── markdown.js ├── passwordPrompt.js ├── plugin.js ├── publishPluginReleaseEvent.js ├── shopwareStoreCommander.js ├── util.js └── version.js ├── package-lock.json └── package.json /.cspell.json: -------------------------------------------------------------------------------- 1 | { 2 | "allowCompoundWords": true, 3 | "words": [ 4 | "Changelogs", 5 | "jsdiff", 6 | "jszip", 7 | "promisify", 8 | "rcompare", 9 | "svenmuennich", 10 | "Shopware" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | end_of_line = lf 10 | # editorconfig-tools is unable to ignore longs strings or urls 11 | max_line_length = null 12 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: "./node_modules/viison-style-guide/javascript/eslintrc.js", 3 | plugins: [ 4 | 'filenames' 5 | ], 6 | rules: { 7 | 'no-console': 'off', 8 | 'max-len': ['warn', 120], 9 | 'filenames/match-regex': 'error', 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/about-codeowners/ for more info 2 | 3 | * @svenmuennich 4 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push] 4 | 5 | # Automatically cancel previous runs for the same ref (i.e. branch) 6 | concurrency: 7 | group: ${{ github.ref }}-${{ github.event_name }} 8 | cancel-in-progress: true 9 | 10 | jobs: 11 | lint: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: actions/setup-node@v4 16 | with: 17 | node-version-file: 'package.json' 18 | cache: 'npm' 19 | cache-dependency-path: 'package-lock.json' 20 | - run: npm ci 21 | - run: npm run eslint 22 | - run: npm run cspell 23 | 24 | publish: 25 | needs: lint 26 | if: startsWith(github.ref, 'refs/tags/') 27 | runs-on: ubuntu-latest 28 | steps: 29 | - uses: actions/checkout@v4 30 | - uses: actions/setup-node@v4 31 | with: 32 | node-version-file: 'package.json' 33 | cache: 'npm' 34 | cache-dependency-path: 'package-lock.json' 35 | registry-url: 'https://registry.npmjs.org' 36 | - run: npm ci 37 | - run: npm publish --access public 38 | env: 39 | NODE_AUTH_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }} 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # nyc test coverage 17 | .nyc_output 18 | 19 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 20 | .grunt 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Optional npm cache directory 26 | .npm 27 | 28 | # Optional REPL history 29 | .node_repl_history 30 | 31 | # Dev files 32 | .cspell.json 33 | .editorconfig 34 | .eslintrc.js 35 | 36 | # GitHub files 37 | .github 38 | -------------------------------------------------------------------------------- /.scs-commander.dist: -------------------------------------------------------------------------------- 1 | # .scs-commander env file template 2 | # 3 | # Set some or all of the following environment variables and save the file 4 | # as .scs-commander in your user's home directory. 5 | # 6 | 7 | # Your Shopware community store username (can be overwritten by passing -u|--username to any command) 8 | # SCS_USERNAME= 9 | 10 | # Your Shopware community store password 11 | # SCS_PASSWORD= 12 | 13 | # Set this option to 1 to disable any 'growl' notifications 14 | # SCS_DISABLE_GROWL=0 15 | 16 | # An optional HTTP webhook endpoint to call after a release 17 | # It supports basic auth and might look like this: https://USERNAME:PASSWORD@domain.tld/endpoint 18 | # SCS_RELEASE_EVENT_ENDPOINT= 19 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 4.0.0 2 | 3 | ### Breaking changes 4 | 5 | * Drops support of Node.js < v18. 6 | 7 | ### Bug fixes 8 | 9 | * Fixes `upload` command. 10 | 11 | ## 3.1.0 12 | 13 | * Enables support for Node.js > v14. 14 | 15 | ## 3.0.2 16 | 17 | * Increase maximum file size for plugin binary uploads to 30MB. 18 | 19 | ## 3.0.1 20 | 21 | * Fixes usages of options passed to any command. 22 | 23 | ## 3.0.0 24 | 25 | ### Breaking changes 26 | 27 | * Drops support of Node.js < v14. 28 | 29 | ### Bug fixes 30 | 31 | * Fixes loading of extra plugin fields. 32 | * Fixes `upload` command. 33 | 34 | ## 2.2.0 35 | 36 | * Adds compatibility with Showpare's 4-digit version numbers. 37 | 38 | ## 2.1.0 39 | 40 | * Adds support for nested lists in changelog markdown. 41 | 42 | ## 2.0.1 43 | 44 | * Fixes evaluation of Shopware version compatibility constraints for Shopware 5 plugins. 45 | 46 | ## 2.0.0 47 | 48 | * Makes commands `changelog` and `upload` compatible with Shopware 6 plugins. 49 | * Makes command `compatibility` fail when executed for a Shopware 6 plugin. 50 | * Fixes a bug in command `dump-plugin` that lead to a crash when trying to dump a plugin that does not exist. 51 | * Removes methods: 52 | * `ShopwareStoreCommander.getSalesForPlugin` 53 | * `ShopwareStoreCommander.getCommissionsForPlugin` 54 | * Removes files: 55 | * `lib/pluginChangelogParser.js` 56 | * `lib/pluginInfo.js` 57 | * `lib/pluginJsonReader.js` 58 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 VIISON GmbH 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 | > [!WARNING] 2 | > **This repository is no longer maintained and v4.0.0 is the final release of the package.** 3 | 4 | # scs-commander 5 | 6 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat)](LICENSE) [![CI](https://github.com/pickware/scs-commander/actions/workflows/ci.yml/badge.svg)](https://github.com/pickware/scs-commander/actions/workflows/ci.yml) [![npm](https://img.shields.io/npm/v/scs-commander.svg?style=flat)](https://www.npmjs.com/package/scs-commander) 7 | 8 | A CLI tool for managing plugins in the Shopware Community Store. 9 | 10 | ## Install 11 | 12 | ### Via npm 13 | 14 | `npm install -g scs-commander` 15 | 16 | ### For development 17 | 18 | Clone this repository and install it using `npm install && npm link`. 19 | 20 | ## Configuration 21 | 22 | You can set your Shopware Community Store username and password in an environment configuration in your user's home directory (`~/.scs-commander`). This file is optional, so you can still pass the username via `-u` to each command and enter your password when asked. Also, even if `~/.scs-commander` exists and contains a username, you can overwrite it by passing the '-u' argument to the command. 23 | Additionally you can set an optional HTTP webhook endpoint in the configuration file as well. This will call the endpoint upon a successful release. The URL supports basic auth and might look like this: `https://USERNAME:PASSWORD@domain.tld/endpoint` 24 | 25 | See [`.scs-commander.dist`](https://github.com/VIISON/scs-commander/blob/master/.scs-commander.dist) for further info. 26 | 27 | ## Usage 28 | 29 | ### List all available plugins 30 | 31 | `scs-commander list -u ` 32 | 33 | You can sort the results by passing option `-s `. The available sort fields are `name`, `version`, `active`, `reviewStatus` and `shopwareCompatibility`. By default only active plugins are listed. If you wish to list all plugins of the account pass `--show-all`. 34 | 35 | ### Update the description of a plugin 36 | 37 | `scs-commander description -u -p -l [--backup] [--patch] [--max-update-retries ] ` 38 | 39 | By default this command reads the file from the provided path and uses its content to update the plugin description in the community store. If you wish to review the resulting changes first and manually confirm them, pass `--patch`. You can also pass `--backup` to back up the current description in the local file system. Since Shopware sometimes randomly returns errors when trying to save a plugin description, you can optionally pass `--max-update-retries ` to automatically retry the upload requests on failure. 40 | 41 | ### Upload a new version of a plugin 42 | 43 | **Remark:** You can only upload plugin `.zip` files, which contain a valid `plugin.json` file next to the plugin `Bootstrap.php` (see [shopwareLabs/plugin-info](https://github.com/shopwareLabs/plugin-info) for more info). 44 | 45 | `scs-commander upload -u ` 46 | 47 | By default, this command automatically requests a review of the uploaded plugin version, which causes the binary to be released automatically. If you only want to upload the binary, pass the `--no_release` or `-R` option. 48 | If set, the HTTP endpoint will get called after a successful release. 49 | 50 | **Note:** Releasing a plugin binary makes only sense when providing a changelog for all available languages, since Shopware requires a changelog of at least 20 characters per supported language. The changelog is parsed directly from a `CHANGELOG.md` file that must be contained in the `.zip` file. The benefit of using a separate `CHANGELOG.md` file is readability, which is why defining a changelog in the `plugin.json` file is not supported. Currently the [changelog parser](https://github.com/VIISON/scs-commander/blob/master/lib/plugin_changelog_parser.js) supports only a simple structure: 51 | 52 | ``` 53 | ## 54 | 55 | ### 56 | 57 | The changelog content of 'version_0' in 'language_A'. Can contain any markdown except for '##' and '###' headlines. 58 | 59 | ### 60 | 61 | The changelog content of 'version_0' in 'language_B'. 62 | 63 | ## 64 | 65 | ### 66 | 67 | The changelog content of 'version_1' in 'language_A'. 68 | 69 | [...] 70 | ``` 71 | 72 | Changelog markdown is compiled to HTML, which is used as the changelog in the store. This makes it easy to add (nested) lists, links and simple formatting (bold, italic etc.) to your plugin changelogs in the community store (and it looks nice in your GitHub repositories too). The order of versions or languages within a version is arbitrary. 73 | 74 | ### Print the changelog of a plugin 75 | 76 | `scs-commander changelog -l ` 77 | 78 | You can also get complied HTML (which is the same as used by the `upload` command) by setting `--html`. If no `language` is provided, the english changelog is returned. 79 | 80 | ### Change the minimum Shopware version compatibility of a plugin 81 | 82 | `scs-commander compatibility -u -p --min-version ` 83 | 84 | ### Download and dump the plugin information 85 | 86 | `scs-commander dump-plugin -u -p [-f ]` 87 | 88 | Currently the only supported output format is `json`. 89 | 90 | ## License 91 | 92 | [MIT](https://github.com/VIISON/scs-commander/blob/master/LICENSE) 93 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const Program = require('commander'); 4 | const version = require('./lib/version'); 5 | 6 | // Define CLI 7 | Program 8 | .version(version) 9 | .command('changelog', 'Echos a plugin zip file\'s changelog.') 10 | .command('compatibility', 'Updates the minimum Shopware version compatibility of all binaries of a plugin.') 11 | .command('description', 'Updates the plugin description of a supported locale.') 12 | .command('dump-plugin', 'Outputs all information for a specified plugin.') 13 | .command('upload', 'Uploads a plugin zip file and makes it available for download.') 14 | .command('list', 'Lists all available plugins.', { isDefault: true }) 15 | .parse(process.argv); 16 | -------------------------------------------------------------------------------- /lib/changelogMarkdownParser.js: -------------------------------------------------------------------------------- 1 | const Markdown = require('./markdown'); 2 | 3 | function parseChangelogMarkdown(markdown) { 4 | const changelog = {}; 5 | let currentVersion; 6 | let currentLocale; 7 | markdown.split('\n').forEach((line) => { 8 | if (line.search(/^##[^#]+/) !== -1) { 9 | // New version 10 | currentVersion = line.substr(2).trim(); 11 | currentLocale = null; 12 | changelog[currentVersion] = {}; 13 | } else if (line.search(/^###[^#]+/) !== -1) { 14 | // New locale 15 | currentLocale = line.substr(3).trim(); 16 | if (!currentVersion) { 17 | throw new Error(`Found new locale section ${currentLocale} without an enclosing version header`); 18 | } 19 | if (changelog[currentVersion][currentLocale]) { 20 | throw new Error(`Locale ${currentLocale} was declared twice for version ${currentVersion}`); 21 | } 22 | changelog[currentVersion][currentLocale] = new Markdown(''); 23 | } else if (currentLocale) { 24 | // Just add the line to the current version/locale pair 25 | changelog[currentVersion][currentLocale].append(`\n${line}`); 26 | } 27 | }); 28 | 29 | return changelog; 30 | } 31 | 32 | module.exports = { parseChangelogMarkdown }; 33 | -------------------------------------------------------------------------------- /lib/cliStatusIndicator.js: -------------------------------------------------------------------------------- 1 | const Chalk = require('chalk'); 2 | const Spinner = require('cli-spinner').Spinner; 3 | const consoleLogger = require('./consoleLogger'); 4 | 5 | // Prepare the loading spinner 6 | const spinner = new Spinner({ 7 | stream: process.stderr, 8 | }); 9 | spinner.setSpinnerString(11); 10 | 11 | function startCLISpinner() { 12 | if (!spinner.isSpinning()) { 13 | spinner.start(); 14 | } 15 | } 16 | 17 | function resetCLISpinner() { 18 | spinner.setSpinnerTitle(''); 19 | if (spinner.isSpinning()) { 20 | spinner.stop(true); 21 | } 22 | } 23 | 24 | /** 25 | * Attach the consoleLogger to the emitter and register spinner related consumers. 26 | * 27 | * @param {EventEmitter} emitter 28 | */ 29 | module.exports = (emitter) => { 30 | consoleLogger(emitter); 31 | emitter.on('loggingIn', () => { 32 | spinner.setSpinnerTitle('Logging in...'); 33 | startCLISpinner(); 34 | }); 35 | emitter.on('startHttpRequest', () => { 36 | startCLISpinner(); 37 | }); 38 | emitter.on('endHttpRequest', () => { 39 | resetCLISpinner(); 40 | }); 41 | emitter.on('loginSuccessful', () => { 42 | resetCLISpinner(); 43 | emitter.emit('info', Chalk.green.bold('Login successful! \u{1F513}')); 44 | }); 45 | emitter.on('waitingReview', () => { 46 | spinner.setSpinnerTitle('Waiting for review to finish...'); 47 | startCLISpinner(); 48 | }); 49 | }; 50 | -------------------------------------------------------------------------------- /lib/client/retryInterceptor.js: -------------------------------------------------------------------------------- 1 | const { promisify } = require('util'); 2 | 3 | const sleep = promisify(setTimeout); 4 | 5 | /** 6 | * Attach the retry interceptor to the axios instance to transparently ensure all requests are going through eventually. 7 | * 8 | * @param {Object} customAxios axios instance 9 | * @return {Object} 10 | */ 11 | module.exports = (customAxios) => { 12 | // on 429 Too Many Requests responses retry after 5 seconds 13 | customAxios.interceptors.response.use( 14 | response => response, 15 | async (error) => { 16 | if (error.response && error.response.status === 429) { 17 | await sleep(5000); 18 | 19 | return customAxios(error.config); 20 | } 21 | 22 | return Promise.reject(error); 23 | }, 24 | ); 25 | 26 | return customAxios; 27 | }; 28 | -------------------------------------------------------------------------------- /lib/client/shopwareAuthenticator.js: -------------------------------------------------------------------------------- 1 | module.exports = class ShopwareAuthenticator { 2 | /** 3 | * @param {String} username 4 | * @param {String} password 5 | * @param {Object} axios instance 6 | */ 7 | constructor(username, password, axios) { 8 | this.accessToken = undefined; 9 | this.userId = undefined; 10 | this.username = username; 11 | this.password = password; 12 | this.axios = axios; 13 | } 14 | 15 | /** 16 | * Login to the Shopware Community Store to obtain the token to authenticate further requests. 17 | * 18 | * @return {String} 19 | * @throws {Error} 20 | */ 21 | async login() { 22 | // Exchange password for access token 23 | this.axios.emitter.emit('loggingIn'); 24 | 25 | try { 26 | const res = await this.axios 27 | .post('accesstokens', { 28 | shopwareId: this.username, 29 | password: this.password, 30 | }, { 31 | noToken: true, 32 | }); 33 | 34 | this.axios.emitter.emit('loginSuccessful'); 35 | 36 | this.accessToken = res.data.token; 37 | this.userId = res.data.userId; 38 | } catch (err) { 39 | if (err.response && err.response.unauthorized) { 40 | throw new Error(`Login failed: ${err.message}`); 41 | } 42 | 43 | throw err; 44 | } 45 | } 46 | 47 | /** 48 | * Handles login and token renewal 49 | */ 50 | registerAuthenticationInterceptors() { 51 | this.registerAccessTokenInterceptor(); 52 | this.registerAuthenticationRenewalInterceptor(); 53 | } 54 | 55 | /** 56 | * The interceptor will cause the client to authenticate if no accessToken is set 57 | */ 58 | registerAccessTokenInterceptor() { 59 | // if no accessToken is stored yet, first login and obtain one 60 | this.axios.interceptors.request.use( 61 | async (config) => { 62 | if (!config.noToken) { 63 | if (!this.accessToken) { 64 | // Login to get an access token 65 | await this.login(); 66 | } 67 | 68 | if (config.headers) { 69 | config.headers['X-Shopware-Token'] = this.accessToken; 70 | } else { 71 | config.headers = { 'X-Shopware-Token': this.accessToken }; 72 | } 73 | } 74 | 75 | return config; 76 | }, 77 | Promise.reject, 78 | ); 79 | } 80 | 81 | /** 82 | * On 401 Unauthenticated responses (token expired) first obtain a new accessToken and then retry the request once 83 | */ 84 | registerAuthenticationRenewalInterceptor() { 85 | this.axios.interceptors.response.use( 86 | response => response, 87 | (error) => { 88 | if (error.response && error.response.status === 401 && error.config && !error.config.isRetryRequest) { 89 | error.config.isRetryRequest = true; 90 | this.accessToken = undefined; 91 | this.axios.emitter.emit('info', 'Renewing auth data...'); 92 | 93 | return this.axios(error.config); 94 | } 95 | 96 | return Promise.reject(error); 97 | }, 98 | ); 99 | } 100 | }; 101 | -------------------------------------------------------------------------------- /lib/client/shopwareStoreClient.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | const spinnerInterceptors = require('./spinnerInterceptors'); 3 | const retryInterceptor = require('./retryInterceptor'); 4 | 5 | const baseURL = 'https://api.shopware.com/'; 6 | const timeout = 2 * 60 * 1000; // 2 minutes 7 | const maxBodyLength = 30 * (1024 ** 2); // 30MB 8 | 9 | /** 10 | * Create the axios instance, attach the emitter to it and register all interceptors. 11 | * 12 | * @param {EventEmitter} emitter 13 | * @return {Object} 14 | */ 15 | module.exports = (emitter) => { 16 | const customAxios = axios.create({ 17 | baseURL, 18 | timeout, 19 | maxBodyLength, 20 | }); 21 | customAxios.emitter = emitter; 22 | spinnerInterceptors(customAxios); 23 | retryInterceptor(customAxios); 24 | 25 | return customAxios; 26 | }; 27 | -------------------------------------------------------------------------------- /lib/client/spinnerInterceptors.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Attach spinner interceptors to the axios instance to transparently ensure the spinner events are emitted. 3 | * 4 | * @param {Object} axios instance 5 | * @return {Object} 6 | */ 7 | module.exports = (customAxios) => { 8 | // start the spinner on request start and stop it on request errors 9 | customAxios.interceptors.request.use( 10 | (config) => { 11 | customAxios.emitter.emit('startHttpRequest'); 12 | 13 | return config; 14 | }, 15 | (error) => { 16 | customAxios.emitter.emit('endHttpRequest'); 17 | 18 | return Promise.reject(error); 19 | }, 20 | ); 21 | 22 | // stop the spinner on responses 23 | customAxios.interceptors.response.use( 24 | (response) => { 25 | customAxios.emitter.emit('endHttpRequest'); 26 | 27 | return response; 28 | }, 29 | (error) => { 30 | customAxios.emitter.emit('endHttpRequest'); 31 | 32 | return Promise.reject(error); 33 | }, 34 | ); 35 | 36 | return customAxios; 37 | }; 38 | -------------------------------------------------------------------------------- /lib/commands/changelog.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const Chalk = require('chalk'); 4 | const path = require('path'); 5 | const Program = require('commander'); 6 | const programVersion = require('../version'); 7 | const Plugin = require('../plugin'); 8 | 9 | // Define CLI 10 | Program 11 | .version(programVersion) 12 | .description('Reads the changelog from a plugin ZIP file and prints it.') 13 | .arguments(' Path to the plugin ZIP file') 14 | .option('-l, --language [language, e.g. "en"]', 'The language for which the changelog shall be returned', 'en') 15 | .option('--html', 'Return the changelog as HTML instead of Markdown') 16 | .parse(process.argv); 17 | 18 | // Load an .env config if available 19 | require('../envLoader').loadConfig(Program); 20 | 21 | // Validate arguments 22 | if (Program.args.length < 1) { 23 | console.error(Chalk.white.bgRed.bold('No file given!')); 24 | process.exit(1); 25 | } 26 | 27 | async function main() { 28 | try { 29 | const pluginZipFilePath = path.resolve(process.cwd(), Program.args[0]); 30 | 31 | const plugin = await Plugin.readFromZipFile(pluginZipFilePath); 32 | 33 | const releaseNotesMarkdown = plugin.releaseNotes[Program.opts().language]; 34 | 35 | if (Program.opts().html) { 36 | console.log(releaseNotesMarkdown.toHtml()); 37 | } else { 38 | console.log(releaseNotesMarkdown.toString()); 39 | } 40 | } catch (err) { 41 | const errorMessage = `\u{1F6AB} Error: ${err.message}`; 42 | console.error(Chalk.white.bgRed.bold(errorMessage)); 43 | console.error(err); 44 | process.exit(-1); 45 | } 46 | } 47 | 48 | main(); 49 | -------------------------------------------------------------------------------- /lib/commands/compatibility.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const Chalk = require('chalk'); 4 | const Program = require('commander'); 5 | const semver = require('semver'); 6 | const { promisify } = require('util'); 7 | 8 | const ShopwareStoreCommander = require('../shopwareStoreCommander'); 9 | const util = require('../util'); 10 | const programVersion = require('../version'); 11 | const spinner = require('../cliStatusIndicator'); 12 | const getPassword = require('../passwordPrompt'); 13 | 14 | const sleep = promisify(setTimeout); 15 | 16 | // Define CLI 17 | Program 18 | .version(programVersion) 19 | .option('-u, --username ', 'The shopware username.') 20 | .option('-p, --plugin ', 'The name of the plugin, whose Shopware version compatibility shall be updated.') 21 | .option('--min-version ', 'The minimum Shopware version compatibility, e.g. 5.1.3') 22 | .parse(process.argv); 23 | 24 | // Load an .env config if available 25 | require('../envLoader').loadConfig(Program); 26 | 27 | // Validate arguments 28 | if (typeof Program.opts().username === 'undefined') { 29 | console.error(Chalk.white.bgRed.bold('No username given!')); 30 | process.exit(1); 31 | } 32 | if (typeof Program.opts().plugin === 'undefined') { 33 | console.error(Chalk.white.bgRed.bold('No plugin name given!')); 34 | process.exit(1); 35 | } 36 | if (typeof Program.opts().minVersion === 'undefined') { 37 | console.error(Chalk.white.bgRed.bold('No minimum Shopware version given!')); 38 | process.exit(1); 39 | } 40 | 41 | async function main() { 42 | try { 43 | const password = await getPassword(Program); 44 | const commander = new ShopwareStoreCommander(Program.opts().username, password); 45 | spinner(commander.logEmitter); 46 | 47 | // Try to find the plugin 48 | const plugin = await commander.findPlugin(Program.opts().plugin); 49 | if (!plugin) { 50 | process.exit(1); 51 | } 52 | 53 | if (plugin.latestBinary.compatibleSoftwareVersions[0].major === 'Shopware 6') { 54 | console.log( 55 | `The plugin ${plugin.name} is only compatible with Shopware 6, which requires Shopware version ` 56 | + 'constraints to be defined in its composer.json file. This is not supported yet.', 57 | ); 58 | process.exit(1); 59 | } 60 | 61 | // Update the minimum Shopware version compatibility of all plugin binaries 62 | const shopwareVersions = commander.statics.softwareVersions; 63 | await util.sequentiallyAwaitEach(plugin.binaries, async (binary) => { 64 | // Determine the current minimum compatible version of the binary 65 | let minCompatibleVersion; 66 | if (binary.compatibleSoftwareVersions.length > 0) { 67 | binary.compatibleSoftwareVersions.sort( 68 | (lhs, rhs) => semver.compare(lhs.name, rhs.name), 69 | ); 70 | minCompatibleVersion = binary.compatibleSoftwareVersions[0].name; 71 | } else { 72 | minCompatibleVersion = shopwareVersions.filter( 73 | version => version.selectable, 74 | ).shift().name; 75 | } 76 | 77 | if (semver.lt(Program.opts().minVersion, minCompatibleVersion)) { 78 | // Add new version compatibility entries to lower the minimum compatibility 79 | console.log(`Lowering minimum compatible Shopware version of binary ${binary.version} to ${Program.opts().minVersion}...`); 80 | binary.compatibleSoftwareVersions = binary.compatibleSoftwareVersions.concat(shopwareVersions.filter( 81 | version => version.selectable 82 | && semver.gte(version.name, Program.opts().minVersion) 83 | && semver.lt(version.name, minCompatibleVersion), 84 | )); 85 | } else if (semver.gt(Program.opts().minVersion, minCompatibleVersion)) { 86 | // Remove some version compatibilities to raise the minimum compatibility 87 | console.log(`Raising minimum compatible Shopware version of binary ${binary.version} to ${Program.opts().minVersion}...`); 88 | binary.compatibleSoftwareVersions = binary.compatibleSoftwareVersions.filter( 89 | version => version.selectable && semver.gte(version.name, Program.opts().minVersion), 90 | ); 91 | } else { 92 | console.log(`Minimum compatible Shopware version of binary ${binary.version} already matches ${Program.opts().minVersion}`); 93 | 94 | return undefined; 95 | } 96 | 97 | if (binary.compatibleSoftwareVersions.length === 0) { 98 | // Add at least the minimum compatible shopware version 99 | binary.compatibleSoftwareVersions = [ 100 | shopwareVersions.find(version => version.selectable && semver.eq(version.name, Program.opts().minVersion)), 101 | ]; 102 | } 103 | 104 | // Save changes after waiting for half a second 105 | await sleep(500); 106 | try { 107 | await commander.savePluginBinary(plugin, binary); 108 | } catch (err) { 109 | const message = `Failed to save binary ${binary.version}: ${err.message}`; 110 | console.error(Chalk.white.bgRed.bold(message)); 111 | } 112 | 113 | return undefined; 114 | }); 115 | 116 | const successMessage = `Minimum Shopware version compatibility of plugin ${plugin.name} changed to ${Program.opts().minVersion}! \u{1F389}`; 117 | util.showGrowlIfEnabled(successMessage); 118 | console.error(Chalk.green.bold(successMessage)); 119 | } catch (err) { 120 | const errorMessage = `\u{1F6AB} Error: ${err.message}`; 121 | util.showGrowlIfEnabled(errorMessage); 122 | console.error(Chalk.white.bgRed.bold(errorMessage)); 123 | console.error(err); 124 | process.exit(-1); 125 | } 126 | } 127 | 128 | main(); 129 | -------------------------------------------------------------------------------- /lib/commands/description.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const Chalk = require('chalk'); 4 | const fs = require('mz/fs'); 5 | const co = require('co'); 6 | const { confirm } = require('co-prompt'); 7 | const jsdiff = require('diff'); 8 | const Program = require('commander'); 9 | const path = require('path'); 10 | const ShopwareStoreCommander = require('../shopwareStoreCommander'); 11 | const util = require('../util'); 12 | const programVersion = require('../version'); 13 | const spinner = require('../cliStatusIndicator'); 14 | const getPassword = require('../passwordPrompt'); 15 | 16 | // Define CLI 17 | Program 18 | .version(programVersion) 19 | .arguments('') 20 | .option('-u, --username ', 'The shopware username.') 21 | .option('-p, --plugin ', 'The name of the plugin, for which the description shall be updated.') 22 | .option('-l, --locale ', 'The locale, for which the description shall be updated, e.g. de_DE, en_GB etc.') 23 | .option('--backup', 'Create a backup file for the old description text') 24 | .option('--patch', 'Show diff and interactively ask before updating') 25 | .option( 26 | '--max-update-retries ', 27 | 'How often to retry updating the description in case of failure', 28 | (givenValue) => { 29 | const parsedCount = parseInt(givenValue, 10); 30 | if (Number.isNaN(parsedCount) || parsedCount < 0) { 31 | throw new Error('retry-count for in --max-update-retries must be a non-negative integer'); 32 | } 33 | 34 | return parsedCount; 35 | }, 36 | 0, 37 | ) 38 | .parse(process.argv); 39 | 40 | // Load an .env config if available 41 | require('../envLoader').loadConfig(Program); 42 | 43 | // Validate arguments 44 | if (Program.args.length < 1) { 45 | console.error(Chalk.white.bgRed.bold('No file given!')); 46 | process.exit(1); 47 | } 48 | if (typeof Program.opts().username === 'undefined') { 49 | console.error(Chalk.white.bgRed.bold('No username given!')); 50 | process.exit(1); 51 | } 52 | if (typeof Program.opts().plugin === 'undefined') { 53 | console.error(Chalk.white.bgRed.bold('No plugin name given!')); 54 | process.exit(1); 55 | } 56 | if (typeof Program.opts().locale === 'undefined') { 57 | console.error(Chalk.white.bgRed.bold('No locale given!')); 58 | process.exit(1); 59 | } 60 | 61 | async function main() { 62 | try { 63 | const password = await getPassword(Program); 64 | const commander = new ShopwareStoreCommander(Program.opts().username, password); 65 | spinner(commander.logEmitter); 66 | 67 | const filePath = path.resolve(process.cwd(), Program.args[0]); 68 | const exists = await fs.exists(filePath); 69 | if (!exists) { 70 | throw new Error(`File ${filePath} does not exist`); 71 | } 72 | 73 | let plugin = await commander.findPlugin(Program.opts().plugin); 74 | if (!plugin) { 75 | process.exit(1); 76 | } 77 | 78 | // Try to find info for locale 79 | const info = plugin.infos.find(data => data.locale.name === Program.opts().locale); 80 | if (!info) { 81 | console.error(Chalk.white.bgRed.bold(`Locale '${Program.opts().locale}' is not available!`)); 82 | console.error(`Available locales for plugin ${plugin.name}`); 83 | plugin.infos.forEach((data) => { 84 | console.error(`- ${data.locale.name}`); 85 | }); 86 | process.exit(1); 87 | } 88 | 89 | // Create a backup file if requested 90 | let backupFilePath = filePath; 91 | if (Program.opts().backup) { 92 | backupFilePath = `${backupFilePath}~`; 93 | await fs.writeFile(backupFilePath, info.description); 94 | console.error('Backup written to ', backupFilePath); 95 | } 96 | 97 | // Read the description file 98 | const description = await fs.readFile(filePath, 'utf-8'); 99 | 100 | if (Program.opts().patch) { 101 | if (info.description === description) { 102 | console.error('No changes'); 103 | } else { 104 | // Write the diff to the output 105 | const unifiedDiff = jsdiff.createTwoFilesPatch(backupFilePath, filePath, info.description, description); 106 | console.log(unifiedDiff); 107 | 108 | // Ask for confirmation to update the description 109 | const confirmed = await co(function* () { 110 | const result = yield confirm('Upload changes? (y/n) '); 111 | // For some reason we have to explicitly pause stdin here (see: https://github.com/tj/co-prompt/issues/9) 112 | process.stdin.pause(); 113 | 114 | return result; 115 | }); 116 | if (!confirmed) { 117 | console.error('Declined uploading changes.'); 118 | process.exit(1); 119 | } 120 | } 121 | } 122 | 123 | // Update plugin description 124 | info.description = description; 125 | 126 | // Save changes 127 | plugin = await commander.savePlugin(plugin, { 128 | retryCount: Program.opts().maxUpdateRetries, 129 | }); 130 | 131 | const successMessage = `Description of plugin ${plugin.name} for locale '${Program.opts().locale}' successfully updated! \u{1F389}`; 132 | util.showGrowlIfEnabled(successMessage); 133 | console.error(Chalk.green.bold(successMessage)); 134 | } catch (err) { 135 | const errorMessage = `\u{1F6AB} Error: ${err.message}`; 136 | util.showGrowlIfEnabled(errorMessage); 137 | console.error(Chalk.white.bgRed.bold(errorMessage)); 138 | console.error(err); 139 | process.exit(-1); 140 | } 141 | } 142 | 143 | main(); 144 | -------------------------------------------------------------------------------- /lib/commands/dumpPlugin.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const Chalk = require('chalk'); 4 | const Program = require('commander'); 5 | const ShopwareStoreCommander = require('../shopwareStoreCommander'); 6 | const util = require('../util'); 7 | const programVersion = require('../version'); 8 | const spinner = require('../cliStatusIndicator'); 9 | const getPassword = require('../passwordPrompt'); 10 | 11 | // Define CLI 12 | Program 13 | .version(programVersion) 14 | .arguments('') 15 | .option('-u, --username ', 'The shopware username.') 16 | .option('-p, --plugin ', 'The name of the plugin for which to output information') 17 | .option('-f, --format ', 'Output in format ', 'json') 18 | .parse(process.argv); 19 | 20 | // Load an .env config if available 21 | require('../envLoader').loadConfig(Program); 22 | 23 | // Validate arguments 24 | if (typeof Program.opts().username === 'undefined') { 25 | console.error(Chalk.white.bgRed.bold('No username given!')); 26 | process.exit(1); 27 | } 28 | if (typeof Program.opts().plugin === 'undefined') { 29 | console.error(Chalk.white.bgRed.bold('No plugin name given!')); 30 | process.exit(1); 31 | } 32 | if (Program.opts().format !== 'json') { 33 | console.error(Chalk.white.bgRed.bold(`Unsupported output format: ${Program.opts().format}`)); 34 | process.exit(1); 35 | } 36 | 37 | async function main() { 38 | try { 39 | const password = await getPassword(Program); 40 | const commander = new ShopwareStoreCommander(Program.opts().username, password); 41 | spinner(commander.logEmitter); 42 | 43 | // Try to find the plugin 44 | const plugin = await commander.findPlugin(Program.opts().plugin); 45 | if (!plugin) { 46 | process.exit(1); 47 | } 48 | 49 | console.log(JSON.stringify(plugin, null, 4)); 50 | } catch (err) { 51 | const errorMessage = `\u{1F6AB} Error: ${err.message}`; 52 | util.showGrowlIfEnabled(errorMessage); 53 | console.error(Chalk.white.bgRed.bold(errorMessage)); 54 | console.error(err); 55 | process.exit(1); 56 | } 57 | } 58 | 59 | main(); 60 | -------------------------------------------------------------------------------- /lib/commands/list.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const Chalk = require('chalk'); 4 | const Program = require('commander'); 5 | const semver = require('semver'); 6 | const Table = require('cli-table'); 7 | const ShopwareStoreCommander = require('../shopwareStoreCommander'); 8 | const programVersion = require('../version'); 9 | const spinner = require('../cliStatusIndicator'); 10 | const getPassword = require('../passwordPrompt'); 11 | 12 | const getShopwareCompatibility = (plugin, reverse) => { 13 | if (!plugin.latestBinary || plugin.latestBinary.compatibleSoftwareVersions.length === 0) { 14 | return null; 15 | } 16 | 17 | const sortedVersions = plugin.latestBinary.compatibleSoftwareVersions 18 | .map(shopwareVersion => shopwareVersion.name) 19 | .sort(semver.compare); 20 | 21 | return (reverse) ? sortedVersions.pop() : sortedVersions.shift(); 22 | }; 23 | 24 | const pluginComparators = { 25 | name: (a, b) => a.name.localeCompare(b.name), 26 | version: (a, b) => { 27 | const vA = (a.latestBinary) ? a.latestBinary.version : '0.0.0'; 28 | const vB = (b.latestBinary) ? b.latestBinary.version : '0.0.0'; 29 | 30 | return semver.compare(vA, vB); 31 | }, 32 | active: (a, b) => a.activationStatus.name.localeCompare(b.activationStatus.name), 33 | reviewStatus: (a, b) => { 34 | const vA = (a.latestBinary) ? a.latestBinary.status.description : 'none'; 35 | const vB = (b.latestBinary) ? b.latestBinary.status.description : 'none'; 36 | 37 | return vA.localeCompare(vB); 38 | }, 39 | releaseDate: (a, b) => { 40 | const dateA = a.latestBinary ? new Date(a.latestBinary.lastChangeDate) : new Date('1970-01-01T00:00:00Z'); 41 | const dateB = b.latestBinary ? new Date(b.latestBinary.lastChangeDate) : new Date('1970-01-01T00:00:00Z'); 42 | 43 | return dateB.getTime() - dateA.getTime(); 44 | }, 45 | minShopwareCompatibility: (a, b) => { 46 | const vA = getShopwareCompatibility(a, false) || '10000.0.0'; 47 | const vB = getShopwareCompatibility(b, false) || '10000.0.0'; 48 | 49 | return semver.compare(vA, vB); 50 | }, 51 | maxShopwareCompatibility: (a, b) => { 52 | const vA = getShopwareCompatibility(a, true) || '10000.0.0'; 53 | const vB = getShopwareCompatibility(b, true) || '10000.0.0'; 54 | 55 | return semver.compare(vA, vB); 56 | }, 57 | }; 58 | 59 | // Define CLI 60 | const availableSortOrders = Object.keys(pluginComparators); 61 | Program 62 | .version(programVersion) 63 | .option('-u, --username ', 'The shopware username') 64 | .option('-s, --sort [value]', `The order in which the plugins will be sorted [${availableSortOrders[0]}] (${availableSortOrders.join('|')})`, availableSortOrders[0]) 65 | .option('--show-all', 'Set this option to list all plugins (incl. disabled plugins)') 66 | .parse(process.argv); 67 | 68 | // Load an .env config if available 69 | require('../envLoader').loadConfig(Program); 70 | 71 | // Validate arguments 72 | if (typeof Program.opts().username === 'undefined') { 73 | console.error(Chalk.white.bgRed.bold('No username given!')); 74 | process.exit(1); 75 | } 76 | if (!pluginComparators[Program.opts().sort]) { 77 | console.error(Chalk.white.bgRed.bold(`'Sort' must be one of ${availableSortOrders.join(', ')}`)); 78 | process.exit(1); 79 | } 80 | 81 | async function main() { 82 | try { 83 | const password = await getPassword(Program); 84 | const commander = new ShopwareStoreCommander(Program.opts().username, password); 85 | spinner(commander.logEmitter); 86 | 87 | const data = await commander.getAccountData(); 88 | let plugins = Object.keys(data.plugins).map(pluginName => data.plugins[pluginName]); 89 | if (!Program.opts().showAll) { 90 | // Filter out all disabled plugins 91 | plugins = plugins.filter(plugin => plugin.activationStatus.id === 1); 92 | } 93 | 94 | // Sort plugins according to the passed sort order 95 | const comparator = pluginComparators[Program.opts().sort]; 96 | const sortedPlugins = plugins.sort(comparator); 97 | 98 | // Print plugin list 99 | console.error('\nYour plugins:'); 100 | const pluginTable = new Table({ 101 | head: ['Name', 'Version', 'Date', 'Active', 'Review status', 'Min. SW version', 'Max. SW version'], 102 | }); 103 | sortedPlugins.forEach((plugin) => { 104 | const version = (plugin.latestBinary) ? plugin.latestBinary.version : '0.0.0'; 105 | const date = (plugin.latestBinary) ? plugin.latestBinary.lastChangeDate : ''; 106 | const active = (plugin.activationStatus.id === 1) ? '\u{2713}' : '\u{274C}'; 107 | const reviewStatus = (plugin.latestBinary) ? plugin.latestBinary.status.description : 'none'; 108 | const minShopwareCompatibility = getShopwareCompatibility(plugin, false) || 'none'; 109 | const maxShopwareCompatibility = getShopwareCompatibility(plugin, true) || 'none'; 110 | pluginTable.push([ 111 | plugin.name, 112 | version, 113 | date, 114 | active, 115 | reviewStatus, 116 | minShopwareCompatibility, 117 | maxShopwareCompatibility, 118 | ]); 119 | }); 120 | console.log(pluginTable.toString()); 121 | } catch (err) { 122 | console.error(Chalk.white.bgRed.bold(`\u{1F6AB} Error: ${err.message}`)); 123 | console.error(err); 124 | process.exit(-1); 125 | } 126 | } 127 | 128 | main(); 129 | -------------------------------------------------------------------------------- /lib/commands/upload.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const Chalk = require('chalk'); 4 | const path = require('path'); 5 | const Program = require('commander'); 6 | const semver = require('semver'); 7 | const ShopwareStoreCommander = require('../shopwareStoreCommander'); 8 | const publishPluginReleaseEvent = require('../publishPluginReleaseEvent'); 9 | const util = require('../util'); 10 | const programVersion = require('../version'); 11 | const spinner = require('../cliStatusIndicator'); 12 | const getPassword = require('../passwordPrompt'); 13 | const Plugin = require('../plugin'); 14 | 15 | function parseOnOffAutoOption(suppliedValue) { 16 | switch (suppliedValue) { 17 | case 'on': 18 | return true; 19 | case 'off': 20 | return false; 21 | case 'auto': 22 | return suppliedValue; 23 | default: 24 | console.error(Chalk.white.bgRed.bold(`Invalid value '${suppliedValue}' for option --license-check-required.`)); 25 | process.exit(-1); 26 | 27 | return undefined; 28 | } 29 | } 30 | 31 | // Define CLI 32 | Program 33 | .version(programVersion) 34 | .arguments('') 35 | .option('-u, --username ', 'The shopware username.') 36 | .option('-R, --no-release', 'Set this option to not submit the uploaded binary for review.') 37 | .option( 38 | '--store-ioncube-encode ', 39 | 'Deprecated: Shopware no longer supports ionCube encryption.', 40 | parseOnOffAutoOption, 41 | 'auto', 42 | ) 43 | .option( 44 | '--license-check-required ', 45 | 'Whether the Store should check for a \'checkLicense\' method in the released binary (default is \'auto\', which retains previous release\'s setting)', 46 | parseOnOffAutoOption, 47 | 'auto', 48 | ) 49 | .option('-f, --force', 'Replace plugin version if it exists') 50 | .parse(process.argv); 51 | 52 | // Load an .env config if available 53 | require('../envLoader').loadConfig(Program); 54 | 55 | // Validate arguments 56 | if (Program.args.length < 1) { 57 | console.error(Chalk.white.bgRed.bold('No file given!')); 58 | process.exit(1); 59 | } 60 | if (typeof Program.opts().username === 'undefined') { 61 | console.error(Chalk.white.bgRed.bold('No username given!')); 62 | process.exit(1); 63 | } 64 | 65 | async function main() { 66 | const pluginZipFilePath = path.resolve(process.cwd(), Program.args[0]); 67 | if (Program.opts().storeIoncubeEncode !== undefined) { 68 | console.error(Chalk.yellow.bold('Warning: Option \'--store-ioncube-encode\' is deprecated because Shopware no longer supports ionCube encryption. Ignoring.')); 69 | } 70 | try { 71 | const password = await getPassword(Program); 72 | const commander = new ShopwareStoreCommander(Program.opts().username, password); 73 | spinner(commander.logEmitter); 74 | 75 | const localPlugin = await Plugin.readFromZipFile(pluginZipFilePath); 76 | // Try to find the plugin 77 | let remotePlugin = await commander.findPlugin(localPlugin.technicalName, ['reviews', 'binaries']); 78 | if (!remotePlugin) { 79 | console.error(`Plugin ${localPlugin.technicalName} does not exist in Community Store.`); 80 | process.exit(1); 81 | } 82 | 83 | // Enable partial ionCube encryption (only applies, if the plugin will be encrypted) 84 | remotePlugin = await commander.enablePartialIonCubeEncryption(remotePlugin); 85 | 86 | // Find the the currently latest binary 87 | const semVerPattern = /(\d+\.\d+\.\d+)([^\d]*)/; 88 | const releasedBinaries = remotePlugin.binaries.filter( 89 | // Filter out binaries that are missing a version or whose review failed 90 | binary => binary.version.length > 0 && binary.status.name === 'codereviewsucceeded', 91 | ).map((binary) => { 92 | const newBinary = { ...binary }; 93 | // Use only the three most significant version parts for comparison 94 | const matches = newBinary.version.match(semVerPattern); 95 | if (matches !== null) { 96 | newBinary.version = matches[1]; 97 | } 98 | 99 | return newBinary; 100 | }).sort( 101 | // Sort by version (semver) and release date (from new to old) 102 | (lhs, rhs) => semver.rcompare(lhs.version, rhs.version) || (-1 * lhs.creationDate.localeCompare(rhs.creationDate)), 103 | ); 104 | const latestReleasedBinary = (releasedBinaries.length > 0) ? releasedBinaries[0] : null; 105 | 106 | if (Program.opts().licenseCheckRequired === 'auto') { 107 | // Set the option based on the latest released binary, if possible 108 | if (latestReleasedBinary) { 109 | Program.opts().licenseCheckRequired = latestReleasedBinary.licenseCheckRequired; 110 | } else { 111 | Program.opts().licenseCheckRequired = false; 112 | console.error(Chalk.yellow.bold('Warning: Cannot automatically determine value for option \'--license-check-required\', because no valid, released binary exists which it could have been derived from. Using \'false\' instead.')); 113 | } 114 | } 115 | 116 | console.error(`Setting version to ${(localPlugin.version)}...`); 117 | const { supportedLocales } = await commander.getAccountData(); 118 | const changelogs = supportedLocales.map((locale) => { 119 | const language = locale.split('_').shift(); 120 | console.error(`Preparing changelog for language '${language}'...`); 121 | // Try to find a changelog 122 | let changelogText = ''; 123 | try { 124 | changelogText = localPlugin.releaseNotes[language].toHtml(); 125 | } catch (e) { 126 | console.error(Chalk.yellow.bold(`\u{26A0} ${e.message}`)); 127 | } 128 | 129 | // Add 20 non-breaking whitespace characters to the changelog. This allows the changelog to pass Shopware's 130 | // server-side validation, which accepts only changelogs with at least 20 characters. 131 | for (let i = 0; i < 20; i += 1) { 132 | changelogText += '\u{0020}'; 133 | } 134 | 135 | return { 136 | locale, 137 | text: changelogText, 138 | }; 139 | }); 140 | const compatibleShopwareVersions = commander.statics.softwareVersions 141 | .filter(version => version.selectable && localPlugin.isCompatibleWithShopwareVersion(version.name)) 142 | .map(version => version.name); 143 | if (compatibleShopwareVersions.length > 0) { 144 | console.error(`Setting shopware version compatibility: ${compatibleShopwareVersions.join(', ')}`); 145 | } else { 146 | console.error( 147 | Chalk.yellow.bold('\u{26A0} Warning: The plugin\'s compatibility constraints don\'t match any available shopware versions!'), 148 | ); 149 | } 150 | 151 | // Make sure that the version of the passed binary does not exist yet 152 | const conflictingBinary = remotePlugin.binaries.find( 153 | binary => binary.version.length > 0 && binary.version === localPlugin.version, 154 | ); 155 | let existingBinary; 156 | if (!conflictingBinary) { 157 | await commander.validatePluginBinaryFile(remotePlugin, pluginZipFilePath); 158 | existingBinary = await commander.createPluginBinary( 159 | remotePlugin, 160 | localPlugin.version, 161 | changelogs, 162 | compatibleShopwareVersions, 163 | ); 164 | } else if (Program.opts().force) { 165 | existingBinary = conflictingBinary; 166 | } else { 167 | throw new Error(`The binary version ${conflictingBinary.version} you're trying to upload already exists for plugin ${remotePlugin.name}`); 168 | } 169 | 170 | let updatedBinary = await commander.uploadPluginBinaryFile( 171 | remotePlugin, 172 | existingBinary, 173 | pluginZipFilePath, 174 | ); 175 | 176 | // Always update the binary after uploading to set the licenseCheckRequired flag because it is not settable 177 | // during creation 178 | updatedBinary = await commander.updatePluginBinary( 179 | remotePlugin, 180 | updatedBinary, 181 | changelogs, 182 | compatibleShopwareVersions, 183 | Program.opts().licenseCheckRequired, 184 | ); 185 | 186 | const uploadSuccessMessage = `New version ${updatedBinary.version} of plugin ${remotePlugin.name} uploaded! \u{2705}`; 187 | console.error(Chalk.green.bold(uploadSuccessMessage)); 188 | if (!Program.opts().release) { 189 | util.showGrowlIfEnabled(uploadSuccessMessage); 190 | console.error(Chalk.yellow.bold('Don\'t forget to manually release the version by requesting a review.')); 191 | process.exit(0); 192 | } 193 | 194 | // Request a review for the uploaded binary 195 | remotePlugin = await commander.requestBinaryReview(remotePlugin); 196 | 197 | // Check review status 198 | const review = remotePlugin.reviews[remotePlugin.reviews.length - 1]; 199 | if (review.status.name !== 'approved') { 200 | throw new Error(`The review of ${remotePlugin.name} v${updatedBinary.version} finished with status '${review.status.name}':\n\n${review.comment}`); 201 | } 202 | 203 | const successMessage = `Review succeeded! Version ${updatedBinary.version} of plugin ${remotePlugin.name} is now available in the store. \u{1F389}`; 204 | util.showGrowlIfEnabled(successMessage); 205 | console.error(Chalk.green.bold(successMessage)); 206 | 207 | await publishPluginReleaseEvent(localPlugin); 208 | } catch (err) { 209 | const errorMessage = `\u{1F6AB} Error: ${err.message}`; 210 | util.showGrowlIfEnabled(errorMessage); 211 | console.error(Chalk.white.bgRed.bold(errorMessage)); 212 | console.error(err); 213 | process.exit(-1); 214 | } 215 | } 216 | 217 | main(); 218 | -------------------------------------------------------------------------------- /lib/consoleLogger.js: -------------------------------------------------------------------------------- 1 | const Chalk = require('chalk'); 2 | 3 | /** 4 | * Register logging related consumers. 5 | * 6 | * @param {EventEmitter} emitter 7 | */ 8 | module.exports = (emitter) => { 9 | emitter.on('error', (message) => { 10 | console.error(Chalk.white.bgRed.bold(message)); 11 | }); 12 | emitter.on('info', (message) => { 13 | console.error(message); 14 | }); 15 | }; 16 | -------------------------------------------------------------------------------- /lib/envLoader.js: -------------------------------------------------------------------------------- 1 | const os = require('os'); 2 | const path = require('path'); 3 | const dotenv = require('dotenv'); 4 | 5 | const defaultMappings = { 6 | username: 'SCS_USERNAME', 7 | password: 'SCS_PASSWORD', 8 | }; 9 | 10 | module.exports = { 11 | 12 | /** 13 | * @param {Object} program 14 | * @param {Object} argumentMappings - optional 15 | */ 16 | loadConfig(program, argumentMappings) { 17 | // Try to load the config from the user home 18 | dotenv.config({ 19 | path: path.resolve(os.homedir(), '.scs-commander'), 20 | silent: true, 21 | }); 22 | 23 | // Check for a 'username' passed as an argument to the program, because we don't want to set the password from 24 | // the .env file, if a different username was passed 25 | const originalUsername = program.opts().username; 26 | 27 | // Copy env values to the program, but don't overwrite passed arguments 28 | const mappings = argumentMappings || defaultMappings; 29 | Object.keys(mappings).forEach((argKey) => { 30 | const envKey = mappings[argKey]; 31 | if (envKey in process.env && !(argKey in program.opts())) { 32 | program.opts()[argKey] = process.env[envKey]; 33 | } 34 | }); 35 | 36 | // Reset the password set in the program, if the passed username and the one now in the program don't match. 37 | // This allows to overwrite the account to be used, even if all account data is set in the .env file. 38 | if ( 39 | originalUsername 40 | && originalUsername.length > 0 41 | && process.env[defaultMappings.username] !== originalUsername 42 | ) { 43 | delete program.opts().password; 44 | } 45 | }, 46 | 47 | }; 48 | -------------------------------------------------------------------------------- /lib/markdown.js: -------------------------------------------------------------------------------- 1 | const marked = require('marked'); 2 | 3 | module.exports = class Markdown { 4 | constructor(markdown) { 5 | this.markdown = markdown; 6 | } 7 | 8 | toString() { 9 | return this.markdown.trim(); 10 | } 11 | 12 | toHtml() { 13 | return marked(this.markdown.trim()); 14 | } 15 | 16 | append(string) { 17 | this.markdown += string; 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /lib/passwordPrompt.js: -------------------------------------------------------------------------------- 1 | const co = require('co'); 2 | const Prompt = require('co-prompt'); 3 | 4 | module.exports = async (program) => { 5 | if (program.opts().password) { 6 | // Use given password 7 | return program.opts().password; 8 | } 9 | 10 | // Prompt for password 11 | return co.wrap(function* () { 12 | return yield Prompt.password('Password: '); 13 | })(); 14 | }; 15 | -------------------------------------------------------------------------------- /lib/plugin.js: -------------------------------------------------------------------------------- 1 | const fs = require('mz/fs'); 2 | const JSZip = require('jszip'); 3 | const path = require('path'); 4 | const semver = require('semver'); 5 | const { parseChangelogMarkdown } = require('./changelogMarkdownParser'); 6 | 7 | function readInfoFromComposerJson(composerJsonString) { 8 | const composerJson = JSON.parse(composerJsonString); 9 | 10 | return { 11 | version: composerJson.version, 12 | shopwareCompatibility: composerJson.require['shopware/core'], 13 | label: { 14 | en: composerJson.extra.label['en-GB'], 15 | de: composerJson.extra.label['de-DE'], 16 | }, 17 | }; 18 | } 19 | 20 | function readInfoFromPluginJson(pluginJsonString) { 21 | const pluginJson = JSON.parse(pluginJsonString); 22 | const minimumVersion = pluginJson.compatibility.minimumVersion || '0.0.0'; 23 | const maximumVersion = pluginJson.compatibility.maximumVersion || '99.99.99'; 24 | 25 | return { 26 | version: pluginJson.currentVersion, 27 | shopwareCompatibility: `${minimumVersion} - ${maximumVersion}`, 28 | label: pluginJson.label, 29 | }; 30 | } 31 | 32 | module.exports = class Plugin { 33 | static async readFromZipFile(zipFilePath) { 34 | if (!await fs.exists(zipFilePath)) { 35 | throw new Error(`File ${zipFilePath} does not exist`); 36 | } 37 | 38 | const data = await fs.readFile(zipFilePath); 39 | const zip = await JSZip.loadAsync(data); 40 | 41 | const sw5RootDirectory = zip.folder(/^(Backend|Frontend|Core)\/\w+\//)[0]; 42 | const sw6RootDirectory = zip.folder(/^\w+\//)[0]; 43 | 44 | const plugin = new Plugin(); 45 | let rootDirectory; 46 | if (sw5RootDirectory) { 47 | rootDirectory = sw5RootDirectory.name; 48 | plugin.technicalName = rootDirectory.split('/')[1]; 49 | plugin.shopwareMajorVersion = 5; 50 | Object.assign(plugin, readInfoFromPluginJson( 51 | await zip.file(path.join(rootDirectory, 'plugin.json')).async('string'), 52 | )); 53 | } else if (sw6RootDirectory) { 54 | rootDirectory = sw6RootDirectory.name; 55 | plugin.technicalName = rootDirectory.split('/')[0]; 56 | plugin.shopwareMajorVersion = 6; 57 | Object.assign(plugin, readInfoFromComposerJson( 58 | await zip.file(path.join(rootDirectory, 'composer.json')).async('string'), 59 | )); 60 | } else { 61 | throw new Error('Could not detect whether plugin targets Shopware 5 or Shopware 6.'); 62 | } 63 | 64 | const parsedChangelog = parseChangelogMarkdown( 65 | await zip.file(path.join(rootDirectory, 'CHANGELOG.md')).async('string'), 66 | ); 67 | plugin.releaseNotes = parsedChangelog[plugin.version]; 68 | 69 | return plugin; 70 | } 71 | 72 | isCompatibleWithShopwareVersion(shopwareMarketingVersion) { 73 | if (!shopwareMarketingVersion) { 74 | return false; 75 | } 76 | 77 | if (this.shopwareMajorVersion === 6) { 78 | if (!shopwareMarketingVersion.startsWith('6.')) { 79 | return false; 80 | } 81 | 82 | const shopwareVersion = this.getShopware6Semver(shopwareMarketingVersion); 83 | const pluginShopwareCompatibility = this.getShopware6Semver(this.shopwareCompatibility); 84 | 85 | return semver.satisfies(shopwareVersion, pluginShopwareCompatibility); 86 | } 87 | 88 | if (this.shopwareMajorVersion === 5) { 89 | if (!shopwareMarketingVersion.startsWith('5.')) { 90 | return false; 91 | } 92 | 93 | return semver.satisfies(shopwareMarketingVersion, this.shopwareCompatibility); 94 | } 95 | 96 | throw new Error( 97 | 'The scs-commander is incompatible with the given Shopware version number' 98 | + ` (${shopwareMarketingVersion}).`, 99 | ); 100 | } 101 | 102 | getShopware6Semver(version) { 103 | if (!version.startsWith('6.')) { 104 | throw new Error(`"${version}" is not a valid Shopware 6 version number.`); 105 | } 106 | 107 | const numberOfVersionComponents = version.split('.').length; 108 | if (numberOfVersionComponents === 4) { 109 | // "New", 4-component version (e.g. `6.3.0.0`). Hence just drop the first component. 110 | return version.substring(2); 111 | } 112 | 113 | if (numberOfVersionComponents === 3) { 114 | // Legacy version (e.g. `6.1.0`). Hence drop the first component and add a patch version of `0`. 115 | return `${version.substring(2)}.0`; 116 | } 117 | 118 | throw new Error('The given shopware version format is unknown.'); 119 | } 120 | }; 121 | -------------------------------------------------------------------------------- /lib/publishPluginReleaseEvent.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | const Chalk = require('chalk'); 3 | 4 | /** 5 | * Create a release event object from a Plugin object. 6 | * 7 | * @param {Plugin} plugin 8 | * @return {Object} 9 | */ 10 | function getReleaseEventInfo(plugin) { 11 | return { 12 | pluginName: plugin.technicalName, 13 | pluginVersion: plugin.version, 14 | pluginLabel: plugin.label, 15 | pluginChangelog: { 16 | de: plugin.releaseNotes.de.toHtml(), 17 | en: plugin.releaseNotes.en.toHtml(), 18 | }, 19 | }; 20 | } 21 | 22 | /** 23 | * If set call the release event endpoint. 24 | * 25 | * @param {Plugin} plugin 26 | * @return {Boolean} 27 | */ 28 | module.exports = async (plugin) => { 29 | if (!process.env.SCS_RELEASE_EVENT_ENDPOINT) { 30 | return; 31 | } 32 | try { 33 | await axios.post(process.env.SCS_RELEASE_EVENT_ENDPOINT, getReleaseEventInfo(plugin)); 34 | console.error(Chalk.green.bold('Publish release event succeeded')); 35 | } catch (err) { 36 | console.error(Chalk.red.bold(`Publish release event failed: ${err.message}`)); 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /lib/shopwareStoreCommander.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const EventEmitter = require('events'); 3 | const { backOff } = require('exponential-backoff'); 4 | const FormData = require('form-data'); 5 | const concat = require('concat-stream'); 6 | const { promisify } = require('util'); 7 | 8 | const ShopwareAuthenticator = require('./client/shopwareAuthenticator'); 9 | const shopwareStoreClient = require('./client/shopwareStoreClient'); 10 | 11 | const sleep = promisify(setTimeout); 12 | 13 | function getPostDataFromFile(filePath) { 14 | // eslint-disable-next-line promise/avoid-new 15 | return new Promise((resolve) => { 16 | const fd = new FormData(); 17 | fd.append('file', fs.createReadStream(filePath)); 18 | fd.pipe(concat({ encoding: 'buffer' }, data => resolve({ 19 | data, 20 | headers: fd.getHeaders(), 21 | }))); 22 | }); 23 | } 24 | 25 | module.exports = class ShopwareStoreCommander { 26 | /** 27 | * @constructor 28 | * @param {String} username 29 | * @param {String} password 30 | * @param {EventEmitter} emitter - optional 31 | */ 32 | constructor(username, password, emitter = new EventEmitter()) { 33 | this.logEmitter = emitter; 34 | this.accountData = null; 35 | this.statics = {}; 36 | this.client = shopwareStoreClient(emitter); 37 | this.shopwareAuthenticator = new ShopwareAuthenticator(username, password, this.client); 38 | this.shopwareAuthenticator.registerAuthenticationInterceptors(); 39 | } 40 | 41 | /** 42 | * @return {Object} 43 | */ 44 | async getAccountData() { 45 | if (this.accountData) { 46 | return this.accountData; 47 | } 48 | 49 | if (!this.shopwareAuthenticator.userId) { 50 | await this.shopwareAuthenticator.login(); 51 | } 52 | 53 | const producers = await this.client.get('producers', { 54 | params: { 55 | companyId: this.shopwareAuthenticator.userId, 56 | }, 57 | }); 58 | 59 | // Save producer ID (required to load e.g. plugins) 60 | this.accountData = { 61 | producerId: producers.data[0].id, 62 | supportedLocales: producers.data[0].supportedLanguages.map(language => language.name), 63 | }; 64 | 65 | const plugins = await this.client.get('plugins', { 66 | params: { 67 | offset: 0, 68 | limit: 1000, 69 | producerId: this.accountData.producerId, 70 | }, 71 | }); 72 | 73 | // Save plugins 74 | this.accountData.plugins = {}; 75 | plugins.data.forEach((plugin) => { 76 | this.accountData.plugins[plugin.name] = plugin; 77 | }); 78 | 79 | // Load static definitions like error codes etc. 80 | const pluginStatics = await this.client.get('pluginstatics/all'); 81 | 82 | this.statics = pluginStatics.data; 83 | 84 | return this.accountData; 85 | } 86 | 87 | /** 88 | * @param {String} pluginName 89 | * @param {Object} extraFields - optional 90 | * @return {Object} 91 | */ 92 | async findPlugin(pluginName, extraFields) { 93 | const accountData = await this.getAccountData(); 94 | 95 | if (!accountData.plugins[pluginName]) { 96 | this.logEmitter.emit('error', `Plugin ${pluginName} not found!`); 97 | 98 | return null; 99 | } 100 | if (extraFields && extraFields.length > 0) { 101 | return this.loadExtraPluginFields(accountData.plugins[pluginName], extraFields); 102 | } 103 | 104 | return accountData.plugins[pluginName]; 105 | } 106 | 107 | /** 108 | * @param {Object} plugin 109 | * @param {Object} [options] 110 | * @param {number} [options.retryCount] how often the save operation should be retried in case of failure 111 | * @return {Object} 112 | */ 113 | async savePlugin(plugin, options) { 114 | this.logEmitter.emit('info', `Saving changes in plugin ${plugin.name}...`); 115 | 116 | const numOfAttempts = ((options && options.retryCount) || 0) + 1; 117 | const res = await backOff( 118 | () => this.client.put(`plugins/${plugin.id}`, plugin), 119 | { numOfAttempts }, 120 | ); 121 | // Save the updated data locally 122 | const accountData = await this.getAccountData(); 123 | this.updatePluginData(accountData.plugins[plugin.name], res.data); 124 | 125 | return accountData.plugins[plugin.name]; 126 | } 127 | 128 | /** 129 | * @param {Object} plugin 130 | * @param {String} version 131 | * @param {Array} changelogs 132 | * @param {Array} compatibleShopwareVersions 133 | * @return {Object} 134 | */ 135 | async createPluginBinary(plugin, version, changelogs, compatibleShopwareVersions) { 136 | this.logEmitter.emit('info', `Creating binary version ${version} of plugin ${plugin.name}...`); 137 | 138 | const accountData = await this.getAccountData(); 139 | const { data: newBinary } = await this.client.post( 140 | `producers/${accountData.producerId}/plugins/${plugin.id}/binaries`, 141 | { 142 | version, 143 | changelogs, 144 | softwareVersions: compatibleShopwareVersions, 145 | }, 146 | ); 147 | 148 | const matchingBinaryIndex = plugin.binaries.findIndex(existingBinary => existingBinary.id === newBinary.id); 149 | if (matchingBinaryIndex === -1) { 150 | plugin.binaries.push(newBinary); 151 | } else { 152 | plugin.binaries[matchingBinaryIndex] = newBinary; 153 | } 154 | plugin.latestBinary = plugin.binaries[plugin.binaries.length - 1]; 155 | 156 | return newBinary; 157 | } 158 | 159 | /** 160 | * @param {Object} plugin 161 | * @param {Object} binary 162 | * @param {Array} changelogs 163 | * @param {Array} compatibleShopwareVersions 164 | * @param {Boolean} licenseCheckRequired 165 | * @return {Object} 166 | */ 167 | async updatePluginBinary(plugin, binary, changelogs, compatibleShopwareVersions, licenseCheckRequired) { 168 | this.logEmitter.emit('info', `Updating binary version ${binary.version} of plugin ${plugin.name}...`); 169 | 170 | const accountData = await this.getAccountData(); 171 | const { data: updatedBinary } = await this.client.put( 172 | `producers/${accountData.producerId}/plugins/${plugin.id}/binaries/${binary.id}`, 173 | { 174 | changelogs, 175 | softwareVersions: compatibleShopwareVersions, 176 | licenseCheckRequired, 177 | // ionCube encryption is no longer supported 178 | ionCubeEncrypted: false, 179 | }, 180 | ); 181 | 182 | const matchingBinaryIndex = plugin.binaries.findIndex(existingBinary => existingBinary.id === binary.id); 183 | if (matchingBinaryIndex !== -1) { 184 | plugin.binaries[matchingBinaryIndex] = updatedBinary; 185 | plugin.latestBinary = plugin.binaries[plugin.binaries.length - 1]; 186 | } 187 | 188 | return updatedBinary; 189 | } 190 | 191 | /** 192 | * @param {Object} plugin 193 | * @param {String} filePath 194 | */ 195 | async validatePluginBinaryFile(plugin, filePath) { 196 | const binaryName = filePath.split(/(\\|\/)/g).pop(); 197 | this.logEmitter.emit('info', `Validating binary ${binaryName} for plugin ${plugin.name}...`); 198 | 199 | const { data, headers } = await getPostDataFromFile(filePath); 200 | const accountData = await this.getAccountData(); 201 | await this.client.post( 202 | `producers/${accountData.producerId}/plugins/${plugin.id}/binaries/validate`, 203 | data, 204 | { headers }, 205 | ); 206 | } 207 | 208 | /** 209 | * @param {Object} plugin 210 | * @param {Object} binary 211 | * @param {String} filePath 212 | * @return {Object} 213 | */ 214 | async uploadPluginBinaryFile(plugin, binary, filePath) { 215 | const binaryName = filePath.split(/(\\|\/)/g).pop(); 216 | this.logEmitter.emit('info', `Uploading binary ${binaryName} for plugin ${plugin.name}...`); 217 | 218 | const { data, headers } = await getPostDataFromFile(filePath); 219 | const accountData = await this.getAccountData(); 220 | const { data: updatedBinary } = await this.client.post( 221 | `producers/${accountData.producerId}/plugins/${plugin.id}/binaries/${binary.id}/file`, 222 | data, 223 | { headers }, 224 | ); 225 | 226 | const matchingBinaryIndex = plugin.binaries.findIndex(existingBinary => existingBinary.id === binary.id); 227 | if (matchingBinaryIndex !== -1) { 228 | plugin.binaries[matchingBinaryIndex] = updatedBinary; 229 | plugin.latestBinary = plugin.binaries[plugin.binaries.length - 1]; 230 | } 231 | 232 | return updatedBinary; 233 | } 234 | 235 | /** 236 | * @param {Object} plugin 237 | * @param {Object} binary 238 | * @return {Object} 239 | */ 240 | async savePluginBinary(plugin, binary) { 241 | this.logEmitter.emit('info', `Saving binary version ${binary.version} of plugin ${plugin.name}...`); 242 | 243 | const accountData = await this.getAccountData(); 244 | const res = await this.client.put( 245 | `producers/${accountData.producerId}/plugins/${plugin.id}/binaries/${binary.id}`, 246 | binary, 247 | ); 248 | // Save the updated data locally 249 | binary.changelogs = res.data.changelogs; 250 | binary.compatibleSoftwareVersions = res.data.compatibleSoftwareVersions; 251 | binary.status = res.data.status; 252 | 253 | return plugin; 254 | } 255 | 256 | /** 257 | * @param {Object} plugin 258 | * @return {Object} 259 | */ 260 | async requestBinaryReview(plugin) { 261 | this.logEmitter.emit('info', `Requesting review of plugin ${plugin.name}...`); 262 | 263 | // "Pending" status IDs: 264 | // 1: pending 265 | // 4: in review 266 | const isReviewPending = review => [1, 4].includes(review.status.id); 267 | 268 | const res = await this.client.post(`plugins/${plugin.id}/reviews`); 269 | // Save the review 270 | const review = res.data[0]; 271 | plugin.reviews.push(review); 272 | 273 | // Wait for the review to finish 274 | let pollCount = 0; 275 | do { 276 | this.logEmitter.emit('waitingReview'); 277 | 278 | // eslint-disable-next-line no-await-in-loop 279 | await sleep(3000); 280 | pollCount += 1; 281 | 282 | // Get review status 283 | // eslint-disable-next-line no-await-in-loop 284 | const reviews = await this.client.get(`plugins/${plugin.id}/reviews`); 285 | // Update polled review 286 | const updatedReview = reviews.data.find(rev => rev.id === review.id); 287 | review.status = updatedReview.status; 288 | review.comment = updatedReview.comment; 289 | } while (isReviewPending(review) && pollCount < 100); 290 | 291 | if (isReviewPending(review)) { 292 | // Max polls reached 293 | throw new Error( 294 | 'Reviews is taking longer than expected. Please check the review status online at ' 295 | + 'https://account.shopware.com/', 296 | ); 297 | } 298 | 299 | return plugin; 300 | } 301 | 302 | /** 303 | * @param {Object} plugin 304 | * @return {Object} 305 | */ 306 | async enablePartialIonCubeEncryption(plugin) { 307 | // Check the plugin for the 'encryptionIonCube' addon 308 | const encryptionAddon = plugin.addons.find(addon => addon.name === 'encryptionIonCube'); 309 | if (!encryptionAddon) { 310 | // The plugin is not encrypted, hence don't enable partial encryption either 311 | return plugin; 312 | } 313 | 314 | // Check the plugin for the 'partialIonCubeEncryptionAllowed' addon 315 | const partialEncryptionAddonName = 'partialIonCubeEncryptionAllowed'; 316 | let partialEncryptionAddon = plugin.addons.find(addon => addon.name === partialEncryptionAddonName); 317 | if (partialEncryptionAddon) { 318 | this.logEmitter.emit('info', `Partial ionCube encryption for plugin ${plugin.name} already enabled`); 319 | 320 | return plugin; 321 | } 322 | 323 | // Find and add the addon for partial ionCube encryption 324 | partialEncryptionAddon = this.statics.addons.find(addon => addon.name === partialEncryptionAddonName); 325 | if (!partialEncryptionAddon) { 326 | throw new Error( 327 | `Cannot enable partial ionCube encryption for plugin ${plugin.name} due to missing plugin addon option`, 328 | ); 329 | } 330 | plugin.addons.push(partialEncryptionAddon); 331 | 332 | this.logEmitter.emit('info', `Enabling partial ionCube encryption for plugin ${plugin.name}...`); 333 | 334 | const res = await this.client.put(`plugins/${plugin.id}`, plugin); 335 | // Save the updated data locally 336 | const accountData = await this.getAccountData(); 337 | this.updatePluginData(accountData.plugins[plugin.name], res.data); 338 | 339 | return accountData.plugins[plugin.name]; 340 | } 341 | 342 | /** 343 | * @param {Object} plugin 344 | * @param {Array} fields 345 | * @return {Object} 346 | */ 347 | async loadExtraPluginFields(plugin, fields) { 348 | plugin.scsLoadedExtraFields = plugin.scsLoadedExtraFields || []; 349 | // Load all extra fields 350 | const accountData = await this.getAccountData(); 351 | const extraFieldPromises = fields.map(async (field) => { 352 | const path = (field === 'binaries') ? `producers/${accountData.producerId}/plugins/${plugin.id}/${field}` : `plugins/${plugin.id}/${field}`; 353 | const res = await this.client.get(path); 354 | plugin[field] = res.data; 355 | // Mark the extra field as loaded 356 | if (plugin.scsLoadedExtraFields.indexOf(field) === -1) { 357 | plugin.scsLoadedExtraFields.push(field); 358 | } 359 | }); 360 | await Promise.all(extraFieldPromises); 361 | 362 | return plugin; 363 | } 364 | 365 | /** 366 | * Apply the given data to the plugin, without overwriting any extra fields that were previously loaded and stored 367 | * in the plugin using 'loadExtraPluginFields()'. 368 | * 369 | * @param {Object} plugin 370 | * @param {Object} data 371 | */ 372 | updatePluginData(plugin, data) { 373 | Object.keys(data).forEach((key) => { 374 | // Only overwrite the field, if it is not a loaded extra field 375 | if (plugin[key] && plugin.scsLoadedExtraFields && plugin.scsLoadedExtraFields.indexOf(key) === -1) { 376 | plugin[key] = data[key]; 377 | } 378 | }); 379 | } 380 | }; 381 | -------------------------------------------------------------------------------- /lib/util.js: -------------------------------------------------------------------------------- 1 | const notifier = require('node-notifier'); 2 | 3 | module.exports = { 4 | 5 | /** 6 | * @param {String} message 7 | */ 8 | showGrowlIfEnabled(message) { 9 | if (parseInt(process.env.SCS_DISABLE_GROWL, 10)) { 10 | return; 11 | } 12 | 13 | notifier.notify({ 14 | title: 'scs-commander', 15 | message, 16 | }); 17 | }, 18 | 19 | /** 20 | * @param {Array} elements 21 | * @param {Function} asyncFn 22 | * @param {Number} index (optional, default 0) 23 | */ 24 | async sequentiallyAwaitEach(elements, asyncFn, index = 0) { 25 | await asyncFn(elements[index]); 26 | if ((index + 1) < elements.length) { 27 | await this.sequentiallyAwaitEach(elements, asyncFn, (index + 1)); 28 | } 29 | }, 30 | 31 | }; 32 | -------------------------------------------------------------------------------- /lib/version.js: -------------------------------------------------------------------------------- 1 | module.exports = require('../package.json').version; 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scs-commander", 3 | "version": "4.0.0", 4 | "description": "A CLI tool for interacting with the Shopware Community Store.", 5 | "keywords": [ 6 | "shopware", 7 | "shopware community store", 8 | "scs", 9 | "shopware plugin" 10 | ], 11 | "bugs": "https://github.com/pickware/scs-commander/issues", 12 | "license": "MIT", 13 | "author": "Pickware GmbH", 14 | "main": "./index.js", 15 | "bin": { 16 | "scs-commander": "./index.js", 17 | "scs-commander-changelog": "./lib/commands/changelog.js", 18 | "scs-commander-compatibility": "./lib/commands/compatibility.js", 19 | "scs-commander-description": "./lib/commands/description.js", 20 | "scs-commander-dump-plugin": "./lib/commands/dumpPlugin.js", 21 | "scs-commander-list": "./lib/commands/list.js", 22 | "scs-commander-upload": "./lib/commands/upload.js" 23 | }, 24 | "directories": { 25 | "lib": "./lib" 26 | }, 27 | "repository": "pickware/scs-commander", 28 | "scripts": { 29 | "cspell": "cspell '**/*.js' README.md .scs-commander.dist .github/CODEOWNERS", 30 | "eslint": "eslint .", 31 | "eslint:fix": "eslint --fix .", 32 | "start": "node ." 33 | }, 34 | "dependencies": { 35 | "axios": "^0.21.1", 36 | "chalk": "^4.1.0", 37 | "cli-spinner": "^0.2.10", 38 | "cli-table": "^0.3.6", 39 | "co": "^4.6.0", 40 | "co-prompt": "^1.0.0", 41 | "commander": "^7.2.0", 42 | "concat-stream": "^2.0.0", 43 | "diff": "^5.0.0", 44 | "dotenv": "^9.0.1", 45 | "exponential-backoff": "^3.1.0", 46 | "form-data": "^4.0.0", 47 | "jszip": "^3.6.0", 48 | "marked": "^2.0.3", 49 | "mz": "^2.4.0", 50 | "node-notifier": "^9.0.1", 51 | "semver": "^7.3.5" 52 | }, 53 | "devDependencies": { 54 | "cspell": "^5.4.0", 55 | "eslint-plugin-filenames": "^1.3.2", 56 | "viison-style-guide": "git+https://github.com/pickware/style-guide.git" 57 | }, 58 | "engines": { 59 | "node": ">=18.0.0" 60 | }, 61 | "preferGlobal": true 62 | } 63 | --------------------------------------------------------------------------------