From 39ddeb94c1b5e49ae1220331bdc6c7814e052d5e Mon Sep 17 00:00:00 2001
From: Daniel Bannert <prisis@users.noreply.github.com>
Date: Tue, 11 Sep 2018 11:45:49 +0200
Subject: [PATCH] adding composer-scripts configurator (#69)

| Q               | A
| --------------- | ---
| Bug fix?        | no
| New feature?    | yes
| BC breaks?      | no
| Deprecations?   | no
| Related tickets | -
| License         | MIT
| Doc PR          | -
---
 doc/CONFIGURATORS.md                          |  26 ++-
 src/Automatic/Automatic.php                   |  14 +-
 src/Automatic/Configurator.php                |  10 +-
 .../ComposerAutoScriptsConfigurator.php       | 100 ++++++++
 .../ComposerScriptsConfigurator.php           | 152 +++++++++++--
 src/Automatic/QuestionFactory.php             |  17 ++
 src/Automatic/ScriptExecutor.php              |   3 +
 .../Configurator/AbstractConfigurator.php     |   2 +-
 .../ComposerAutoScriptsConfiguratorTest.php   | 144 ++++++++++++
 .../ComposerScriptsConfiguratorTest.php       | 215 ++++++++++++++++--
 .../CopyFromPackageConfiguratorTest.php       |  42 ++--
 .../Configurator/EnvConfiguratorTest.php      |   6 +-
 .../GitignoreConfiguratorTest.php             |   2 +-
 tests/Automatic/ConfiguratorTest.php          |  14 +-
 tests/Automatic/PackageConfiguratorTest.php   |   4 +-
 15 files changed, 665 insertions(+), 86 deletions(-)
 create mode 100644 src/Automatic/Configurator/ComposerAutoScriptsConfigurator.php
 create mode 100644 tests/Automatic/Configurator/ComposerAutoScriptsConfiguratorTest.php

diff --git a/doc/CONFIGURATORS.md b/doc/CONFIGURATORS.md
index 5a35f8a2..14142613 100644
--- a/doc/CONFIGURATORS.md
+++ b/doc/CONFIGURATORS.md
@@ -9,7 +9,7 @@ Configurators define the different tasks executed when installing a dependency,
 
 The package only contain the tasks needed to install and configure the dependency, because Narrowspark Automatic Configurators are smart enough to reverse those tasks when uninstalling and unconfiguring the dependencies.
 
-Narrowspark Automatic comes with several types of tasks, which are called **configurators**: `copy`, `env`, `composer-scripts`, `gitignore` and a special configurator `post-install-output`.
+Narrowspark Automatic comes with several types of tasks, which are called **configurators**: `copy`, `env`, `composer-scripts`, `composer-auto-scripts`, `gitignore` and a special configurator `post-install-output`.
 
 ### Copy Configurator `copy`
 
@@ -30,7 +30,7 @@ directory of the application:
 
 The `%BIN_DIR%` string is a special value that it's turned into the absolute
 path of the binaries directory. You can access any variable defined in
-the `extra` section of your `composer.json` file:
+the `extra` section of your root `composer.json` file:
 
 ```json
 {
@@ -72,22 +72,38 @@ The `###> your-package-name-here ###` section separators are needed by Narrowspa
 to detect the contents added by this dependency in case you uninstall it later.
 > !!! Don't remove or modify these separators.
 
-Composer Scripts Configurator `composer-scripts`
+Composer Auto Scripts Configurator `composer-auto-scripts`
 
-Registers scripts in the `auto-scripts` section of the `composer.json` file
+Registers `auto-scripts` in the `composer-scripts` section of the root `composer.json` file
 to execute them automatically when running `composer install` and `composer update`.
 The value is an associative array where the key is the script to execute (including all its arguments and options) and the value is the type of script (`php-script` for PHP scripts, ``script`` for any shell script):
 
 ```json
 {
     "configurators": {
-        "composer-scripts": {
+        "composer-auto-scripts": {
             "echo \"hallo\";": "php-script",
             "bash -c \"echo hallo\"": "script"
         }
     }
 }
 ```
+Composer Scripts Configurator `composer-scripts`
+
+Registers [composer scripts](https://getcomposer.org/doc/articles/scripts.md) in the `composer-scripts` section of the root `composer.json` file.
+Only the composer `command`, `installer` and `package` events are supported, you will get a warning if other events a used.
+
+```json
+{
+    "configurators": {
+        "composer-auto-scripts": {
+            "post-autoload-dump" : [
+                "Your\\Namespace\\ComposerScripts::dump"
+            ]
+        }
+    }
+}
+```
 
 You can create your own script executor, create a new class inside your Package Repository in a `Automatic` folder and extend `Narrowspark\Automatic\Common\ScriptExtender\AbstractScriptExtender` the example below shows you how it should look:
 
diff --git a/src/Automatic/Automatic.php b/src/Automatic/Automatic.php
index ec2141ec..d94cdea7 100644
--- a/src/Automatic/Automatic.php
+++ b/src/Automatic/Automatic.php
@@ -155,13 +155,13 @@ public static function getSubscribedEvents(): array
             InstallerEvents::POST_DEPENDENCIES_SOLVING => [['populateFilesCacheDir', \PHP_INT_MAX]],
             PackageEvents::PRE_PACKAGE_INSTALL         => [['populateFilesCacheDir', ~\PHP_INT_MAX]],
             PackageEvents::PRE_PACKAGE_UPDATE          => [['populateFilesCacheDir', ~\PHP_INT_MAX]],
-            PackageEvents::POST_PACKAGE_INSTALL        => [['record'], ['auditPackage']],
-            PackageEvents::POST_PACKAGE_UPDATE         => [['record'], ['auditPackage']],
+            PackageEvents::POST_PACKAGE_INSTALL        => [['record'], ['auditPackage', \PHP_INT_MAX]],
+            PackageEvents::POST_PACKAGE_UPDATE         => [['record'], ['auditPackage', \PHP_INT_MAX]],
             PackageEvents::POST_PACKAGE_UNINSTALL      => 'record',
             PluginEvents::PRE_FILE_DOWNLOAD            => 'onFileDownload',
-            ScriptEvents::POST_INSTALL_CMD             => [['onPostInstall'], ['auditComposerLock']],
-            ScriptEvents::POST_UPDATE_CMD              => [['onPostUpdate'], ['auditComposerLock']],
-            ScriptEvents::POST_CREATE_PROJECT_CMD      => [['onPostCreateProject', \PHP_INT_MAX], ['runSkeletonGenerator']],
+            ScriptEvents::POST_INSTALL_CMD             => [['onPostInstall', \PHP_INT_MAX - 1], ['auditComposerLock', \PHP_INT_MAX]],
+            ScriptEvents::POST_UPDATE_CMD              => [['onPostUpdate', \PHP_INT_MAX - 1], ['auditComposerLock', \PHP_INT_MAX]],
+            ScriptEvents::POST_CREATE_PROJECT_CMD      => [['onPostCreateProject', \PHP_INT_MAX], ['runSkeletonGenerator', ~\PHP_INT_MAX]],
         ];
     }
 
@@ -1121,13 +1121,13 @@ private function showWarningOnRemainingConfigurators(
     ): void {
         $packageConfigurators = \array_keys((array) $package->getConfig(ConfiguratorContract::TYPE));
 
-        foreach (\array_keys($configurator->getConfigurators()) as $key) {
+        foreach (\array_keys($configurator->getConfigurators()) as $key => $value) {
             if (isset($packageConfigurators[$key])) {
                 unset($packageConfigurators[$key]);
             }
         }
 
-        foreach (\array_keys($packageConfigurator->getConfigurators()) as $key) {
+        foreach (\array_keys($packageConfigurator->getConfigurators()) as $key => $value) {
             if (isset($packageConfigurators[$key])) {
                 unset($packageConfigurators[$key]);
             }
diff --git a/src/Automatic/Configurator.php b/src/Automatic/Configurator.php
index db955963..2babcb2e 100644
--- a/src/Automatic/Configurator.php
+++ b/src/Automatic/Configurator.php
@@ -3,6 +3,7 @@
 namespace Narrowspark\Automatic;
 
 use Narrowspark\Automatic\Common\Contract\Configurator as ConfiguratorContract;
+use Narrowspark\Automatic\Configurator\ComposerAutoScriptsConfigurator;
 use Narrowspark\Automatic\Configurator\ComposerScriptsConfigurator;
 use Narrowspark\Automatic\Configurator\CopyFromPackageConfigurator;
 use Narrowspark\Automatic\Configurator\EnvConfigurator;
@@ -16,10 +17,11 @@ final class Configurator extends AbstractConfigurator
      * @var array
      */
     protected $configurators = [
-        'composer-scripts' => ComposerScriptsConfigurator::class,
-        'copy'             => CopyFromPackageConfigurator::class,
-        'env'              => EnvConfigurator::class,
-        'gitignore'        => GitIgnoreConfigurator::class,
+        'composer-auto-scripts' => ComposerAutoScriptsConfigurator::class,
+        'composer-scripts'      => ComposerScriptsConfigurator::class,
+        'copy'                  => CopyFromPackageConfigurator::class,
+        'env'                   => EnvConfigurator::class,
+        'gitignore'             => GitIgnoreConfigurator::class,
     ];
 
     /**
diff --git a/src/Automatic/Configurator/ComposerAutoScriptsConfigurator.php b/src/Automatic/Configurator/ComposerAutoScriptsConfigurator.php
new file mode 100644
index 00000000..6c1c549f
--- /dev/null
+++ b/src/Automatic/Configurator/ComposerAutoScriptsConfigurator.php
@@ -0,0 +1,100 @@
+<?php
+declare(strict_types=1);
+namespace Narrowspark\Automatic\Configurator;
+
+use Composer\Composer;
+use Composer\IO\IOInterface;
+use Narrowspark\Automatic\Common\Configurator\AbstractConfigurator;
+use Narrowspark\Automatic\Common\Contract\Configurator as ConfiguratorContract;
+use Narrowspark\Automatic\Common\Contract\Package as PackageContract;
+use Narrowspark\Automatic\Common\Util;
+
+final class ComposerAutoScriptsConfigurator extends AbstractConfigurator
+{
+    /**
+     * A json instance.
+     *
+     * @var \Composer\Json\JsonFile
+     */
+    private $json;
+
+    /**
+     * A json manipulator instance.
+     *
+     * @var \Composer\Json\JsonManipulator
+     */
+    private $manipulator;
+
+    /**
+     * {@inheritdoc}
+     */
+    public function __construct(Composer $composer, IOInterface $io, array $options = [])
+    {
+        parent::__construct($composer, $io, $options);
+
+        [$json, $manipulator] = Util::getComposerJsonFileAndManipulator();
+
+        $this->json        = $json;
+        $this->manipulator = $manipulator;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public static function getName(): string
+    {
+        return 'composer-auto-scripts';
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function configure(PackageContract $package): void
+    {
+        $autoScripts = $this->getComposerAutoScripts();
+
+        $autoScripts = \array_merge($autoScripts, (array) $package->getConfig(ConfiguratorContract::TYPE, self::getName()));
+
+        $this->manipulateAndWrite($autoScripts);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function unconfigure(PackageContract $package): void
+    {
+        $autoScripts = $this->getComposerAutoScripts();
+
+        foreach (\array_keys((array) $package->getConfig(ConfiguratorContract::TYPE, self::getName())) as $cmd) {
+            unset($autoScripts[$cmd]);
+        }
+
+        $this->manipulateAndWrite($autoScripts);
+    }
+
+    /**
+     * Get root composer.json content and the auto-scripts section.
+     *
+     * @return array
+     */
+    private function getComposerAutoScripts(): array
+    {
+        $jsonContents = $this->json->read();
+
+        return $jsonContents['scripts']['auto-scripts'] ?? [];
+    }
+
+    /**
+     * Manipulate the root composer.json with given auto-scripts.
+     *
+     * @param array $autoScripts
+     *
+     * @return void
+     */
+    private function manipulateAndWrite(array $autoScripts): void
+    {
+        $this->manipulator->addSubNode('scripts', 'auto-scripts', $autoScripts);
+
+        \file_put_contents($this->json->getPath(), $this->manipulator->getContents());
+    }
+}
diff --git a/src/Automatic/Configurator/ComposerScriptsConfigurator.php b/src/Automatic/Configurator/ComposerScriptsConfigurator.php
index 98e737e6..0771ccdf 100644
--- a/src/Automatic/Configurator/ComposerScriptsConfigurator.php
+++ b/src/Automatic/Configurator/ComposerScriptsConfigurator.php
@@ -3,13 +3,28 @@
 namespace Narrowspark\Automatic\Configurator;
 
 use Composer\Composer;
+use Composer\Installer\InstallerEvents;
+use Composer\Installer\PackageEvents;
 use Composer\IO\IOInterface;
+use Composer\Script\ScriptEvents;
 use Narrowspark\Automatic\Common\Configurator\AbstractConfigurator;
+use Narrowspark\Automatic\Common\Contract\Configurator as ConfiguratorContract;
 use Narrowspark\Automatic\Common\Contract\Package as PackageContract;
 use Narrowspark\Automatic\Common\Util;
+use Narrowspark\Automatic\QuestionFactory;
 
 final class ComposerScriptsConfigurator extends AbstractConfigurator
 {
+    /**
+     * @var string
+     */
+    private const WHITELIST = 'composer-script-whitelist';
+
+    /**
+     * @var string
+     */
+    private const BLACKLIST = 'composer-script-blacklist';
+
     /**
      * A json instance.
      *
@@ -24,6 +39,34 @@ final class ComposerScriptsConfigurator extends AbstractConfigurator
      */
     private $manipulator;
 
+    /**
+     * All allowed composer scripts.
+     *
+     * @var array
+     */
+    private $allowedComposerEvents = [
+        ScriptEvents::POST_ARCHIVE_CMD,
+        ScriptEvents::POST_AUTOLOAD_DUMP,
+        ScriptEvents::POST_CREATE_PROJECT_CMD,
+        ScriptEvents::POST_INSTALL_CMD,
+        ScriptEvents::POST_ROOT_PACKAGE_INSTALL,
+        ScriptEvents::POST_STATUS_CMD,
+        ScriptEvents::POST_UPDATE_CMD,
+        ScriptEvents::PRE_ARCHIVE_CMD,
+        ScriptEvents::PRE_AUTOLOAD_DUMP,
+        ScriptEvents::PRE_INSTALL_CMD,
+        ScriptEvents::PRE_STATUS_CMD,
+        ScriptEvents::PRE_UPDATE_CMD,
+        InstallerEvents::POST_DEPENDENCIES_SOLVING,
+        InstallerEvents::PRE_DEPENDENCIES_SOLVING,
+        PackageEvents::POST_PACKAGE_INSTALL,
+        PackageEvents::POST_PACKAGE_UNINSTALL,
+        PackageEvents::POST_PACKAGE_UPDATE,
+        PackageEvents::PRE_PACKAGE_INSTALL,
+        PackageEvents::PRE_PACKAGE_UNINSTALL,
+        PackageEvents::PRE_PACKAGE_UPDATE,
+    ];
+
     /**
      * {@inheritdoc}
      */
@@ -31,10 +74,7 @@ public function __construct(Composer $composer, IOInterface $io, array $options
     {
         parent::__construct($composer, $io, $options);
 
-        [$json, $manipulator] = Util::getComposerJsonFileAndManipulator();
-
-        $this->json        = $json;
-        $this->manipulator = $manipulator;
+        [$this->json, $this->manipulator] = Util::getComposerJsonFileAndManipulator();
     }
 
     /**
@@ -50,11 +90,67 @@ public static function getName(): string
      */
     public function configure(PackageContract $package): void
     {
-        $autoScripts = $this->getComposerContentAndAutoScripts();
+        $packageEvents = (array) $package->getConfig(ConfiguratorContract::TYPE, self::getName());
+
+        if (\count($packageEvents) === 0) {
+            return;
+        }
+
+        $composerContent = $this->json->read();
+
+        if (isset($composerContent['extra'][Util::COMPOSER_EXTRA_KEY][self::BLACKLIST])) {
+            $blackList = \array_flip($composerContent['extra'][Util::COMPOSER_EXTRA_KEY][self::BLACKLIST]);
+
+            if (isset($blackList[$package->getName()])) {
+                $this->io->write(\sprintf('Composer scripts for [%s] skipped, because it was found in the [%s]', $package->getPrettyName(), self::BLACKLIST));
+
+                return;
+            }
+        }
 
-        $autoScripts = \array_merge($autoScripts, (array) $package->getConfig(self::getName()));
+        $allowedEvents = [];
 
-        $this->manipulateAndWrite($autoScripts);
+        foreach ($this->allowedComposerEvents as $event) {
+            if (isset($packageEvents[$event])) {
+                $allowedEvents[$event] = (array) $packageEvents[$event];
+
+                unset($packageEvents[$event]);
+            }
+        }
+
+        $allowed = false;
+
+        if (\count($allowedEvents) !== 0) {
+            if (isset($composerContent['extra'][Util::COMPOSER_EXTRA_KEY][self::WHITELIST])) {
+                $whiteList = \array_flip($composerContent['extra'][Util::COMPOSER_EXTRA_KEY][self::WHITELIST]);
+
+                if (isset($whiteList[$package->getName()])) {
+                    $allowed = true;
+                }
+            }
+
+            if ($allowed === false) {
+                $allowed = $this->io->askConfirmation(QuestionFactory::getPackageScriptsQuestion($package->getPrettyName()), false);
+            }
+        }
+
+        if (\count($packageEvents) !== 0) {
+            $this->io->write(\sprintf(
+                '<warning>    Found not allowed composer events [%s] in [%s]</>' . \PHP_EOL,
+                \implode(', ', \array_keys($packageEvents)),
+                $package->getName()
+            ));
+        }
+
+        if ($allowed) {
+            $this->manipulator->addSubNode(
+                'extra',
+                Util::COMPOSER_EXTRA_KEY,
+                \array_merge($composerContent['extra'][Util::COMPOSER_EXTRA_KEY] ?? [], [self::WHITELIST => [$package->getName()]])
+            );
+
+            $this->manipulateAndWrite(\array_merge($this->getComposerScripts(), $allowedEvents));
+        }
     }
 
     /**
@@ -62,13 +158,33 @@ public function configure(PackageContract $package): void
      */
     public function unconfigure(PackageContract $package): void
     {
-        $autoScripts = $this->getComposerContentAndAutoScripts();
+        $composerScripts = $this->getComposerScripts();
+
+        foreach ((array) $package->getConfig(ConfiguratorContract::TYPE, self::getName()) as $key => $scripts) {
+            foreach ((array) $scripts as $script) {
+                if (isset($this->allowedComposerEvents[$key], $composerScripts[$key][$script])) {
+                    unset($composerScripts[$key][$script]);
+                }
+            }
+        }
 
-        foreach (\array_keys((array) $package->getConfig(self::getName())) as $cmd) {
-            unset($autoScripts[$cmd]);
+        $composerContent = $this->json->read();
+
+        if (isset($composerContent['extra'][Util::COMPOSER_EXTRA_KEY][self::WHITELIST])) {
+            $whiteList = \array_flip($composerContent['extra'][Util::COMPOSER_EXTRA_KEY][self::WHITELIST]);
+
+            if (isset($whiteList[$package->getName()])) {
+                unset($composerContent['extra'][Util::COMPOSER_EXTRA_KEY][self::WHITELIST][$whiteList[$package->getName()]]);
+
+                $this->manipulator->addSubNode(
+                    'extra',
+                    Util::COMPOSER_EXTRA_KEY,
+                    $composerContent['extra'][Util::COMPOSER_EXTRA_KEY]
+                );
+            }
         }
 
-        $this->manipulateAndWrite($autoScripts);
+        $this->manipulateAndWrite($composerScripts);
     }
 
     /**
@@ -76,21 +192,23 @@ public function unconfigure(PackageContract $package): void
      *
      * @return array
      */
-    private function getComposerContentAndAutoScripts(): array
+    private function getComposerScripts(): array
     {
         $jsonContents = $this->json->read();
 
-        return $jsonContents['scripts']['auto-scripts'] ?? [];
+        return $jsonContents['scripts'] ?? [];
     }
 
     /**
-     * Manipulate the root composer.json with given auto-scripts.
+     * Manipulate the root composer.json with given scripts.
+     *
+     * @param array $scripts
      *
-     * @param array $autoScripts
+     * @return void
      */
-    private function manipulateAndWrite(array $autoScripts): void
+    private function manipulateAndWrite(array $scripts): void
     {
-        $this->manipulator->addSubNode('scripts', 'auto-scripts', $autoScripts);
+        $this->manipulator->addMainKey('scripts', $scripts);
 
         \file_put_contents($this->json->getPath(), $this->manipulator->getContents());
     }
diff --git a/src/Automatic/QuestionFactory.php b/src/Automatic/QuestionFactory.php
index d050effd..bdbcb849 100644
--- a/src/Automatic/QuestionFactory.php
+++ b/src/Automatic/QuestionFactory.php
@@ -32,6 +32,23 @@ public static function getPackageQuestion(string $name, ?string $url): string
         return \sprintf("    Review the package from %s.\n" . $message, \str_replace('.git', '', $url), $name);
     }
 
+    /**
+     * Returns the questions for package scripts.
+     *
+     * @param string $name
+     *
+     * @return string
+     */
+    public static function getPackageScriptsQuestion(string $name): string
+    {
+        $message = <<<'PHP'
+    Do you want to add this package [%s] composer scripts?
+    (defaults to <comment>no</comment>): 
+PHP;
+
+        return \sprintf($message, $name);
+    }
+
     /**
      * Validate given input answer.
      *
diff --git a/src/Automatic/ScriptExecutor.php b/src/Automatic/ScriptExecutor.php
index 2c055a9e..53f7c208 100644
--- a/src/Automatic/ScriptExecutor.php
+++ b/src/Automatic/ScriptExecutor.php
@@ -16,6 +16,9 @@ final class ScriptExecutor
 {
     use ExpandTargetDirTrait;
 
+    /**
+     * @var string
+     */
     public const TYPE = 'script-extenders';
 
     /**
diff --git a/src/Common/Configurator/AbstractConfigurator.php b/src/Common/Configurator/AbstractConfigurator.php
index 7ac57627..b9066fa2 100644
--- a/src/Common/Configurator/AbstractConfigurator.php
+++ b/src/Common/Configurator/AbstractConfigurator.php
@@ -76,7 +76,7 @@ protected function write($messages): void
         }
 
         foreach ($messages as $i => $message) {
-            $messages[$i] = '    · ' . $message;
+            $messages[$i] = '    - ' . $message;
         }
 
         $this->io->writeError($messages, true, IOInterface::VERBOSE);
diff --git a/tests/Automatic/Configurator/ComposerAutoScriptsConfiguratorTest.php b/tests/Automatic/Configurator/ComposerAutoScriptsConfiguratorTest.php
new file mode 100644
index 00000000..eee43011
--- /dev/null
+++ b/tests/Automatic/Configurator/ComposerAutoScriptsConfiguratorTest.php
@@ -0,0 +1,144 @@
+<?php
+declare(strict_types=1);
+namespace Narrowspark\Automatic\Test\Configurator;
+
+use Composer\Composer;
+use Composer\IO\IOInterface;
+use Composer\Json\JsonFile;
+use Composer\Json\JsonManipulator;
+use Narrowspark\Automatic\Common\Contract\Configurator as ConfiguratorContract;
+use Narrowspark\Automatic\Common\Contract\Package as PackageContract;
+use Narrowspark\Automatic\Common\Traits\GetGenericPropertyReaderTrait;
+use Narrowspark\Automatic\Configurator\ComposerAutoScriptsConfigurator;
+use Narrowspark\Automatic\Test\Fixture\ComposerJsonFactory;
+use Narrowspark\TestingHelper\Phpunit\MockeryTestCase;
+
+/**
+ * @internal
+ */
+final class ComposerAutoScriptsConfiguratorTest extends MockeryTestCase
+{
+    use GetGenericPropertyReaderTrait;
+
+    /**
+     * @var \Composer\Composer
+     */
+    private $composer;
+
+    /**
+     * @var \Composer\IO\IOInterface|\Mockery\MockInterface
+     */
+    private $ioMock;
+
+    /**
+     * @var \Composer\Json\JsonFile|\Mockery\MockInterface
+     */
+    private $jsonMock;
+
+    /**
+     * @var \Composer\Json\JsonManipulator|\Mockery\MockInterface
+     */
+    private $jsonManipulatorMock;
+
+    /**
+     * @var \Narrowspark\Automatic\Configurator\ComposerAutoScriptsConfigurator
+     */
+    private $configurator;
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function setUp(): void
+    {
+        parent::setUp();
+
+        $this->jsonMock            = $this->mock(JsonFile::class);
+        $this->jsonManipulatorMock = $this->mock(JsonManipulator::class);
+
+        $this->composer = new Composer();
+        $this->ioMock   = $this->mock(IOInterface::class);
+
+        $this->configurator = new ComposerAutoScriptsConfigurator($this->composer, $this->ioMock, ['self-dir' => 'test']);
+
+        $callback = $this->getGenericPropertyReader();
+
+        $json = &$callback($this->configurator, 'json');
+        $json = $this->jsonMock;
+
+        $manipulator = &$callback($this->configurator, 'manipulator');
+        $manipulator = $this->jsonManipulatorMock;
+    }
+
+    public function testGetName(): void
+    {
+        static::assertSame('composer-auto-scripts', ComposerAutoScriptsConfigurator::getName());
+    }
+
+    public function testConfigure(): void
+    {
+        $composerRootJsonString = ComposerJsonFactory::createComposerScriptJson('configure', ['auto-scripts' => []]);
+        $composerRootJsonData   = ComposerJsonFactory::jsonToArray($composerRootJsonString);
+
+        $script = ['php -v' => 'script'];
+
+        $packageMock = $this->mock(PackageContract::class);
+        $packageMock->shouldReceive('getConfig')
+            ->once()
+            ->with(ConfiguratorContract::TYPE, ComposerAutoScriptsConfigurator::getName())
+            ->andReturn($script);
+
+        $this->jsonMock->shouldReceive('read')
+            ->andReturn($composerRootJsonData);
+
+        $composerJsonPath = __DIR__ . '/composer.json';
+
+        $this->jsonMock->shouldReceive('getPath')
+            ->once()
+            ->andReturn($composerJsonPath);
+
+        $this->jsonManipulatorMock->shouldReceive('addSubNode')
+            ->once()
+            ->with('scripts', 'auto-scripts', $script);
+
+        $composerRootJsonData['scripts']['auto-scripts'] = $script;
+
+        $this->jsonManipulatorMock->shouldReceive('getContents')
+            ->andReturn(ComposerJsonFactory::arrayToJson($composerRootJsonData));
+
+        $this->configurator->configure($packageMock);
+
+        \unlink($composerJsonPath);
+    }
+
+    public function testUnconfigure(): void
+    {
+        $composerRootJsonString = ComposerJsonFactory::createComposerScriptJson('unconfigure', ['auto-scripts' => ['php -v' => 'script', 'list' => 'cerebro-cmd']]);
+        $composerRootJsonData   = ComposerJsonFactory::jsonToArray($composerRootJsonString);
+
+        $packageMock = $this->mock(PackageContract::class);
+        $packageMock->shouldReceive('getConfig')
+            ->once()
+            ->with(ConfiguratorContract::TYPE, ComposerAutoScriptsConfigurator::getName())
+            ->andReturn(['php -v' => 'script']);
+
+        $this->jsonMock->shouldReceive('read')
+            ->andReturn($composerRootJsonData);
+
+        $composerJsonPath = __DIR__ . '/composer.json';
+
+        $this->jsonMock->shouldReceive('getPath')
+            ->once()
+            ->andReturn($composerJsonPath);
+
+        $this->jsonManipulatorMock->shouldReceive('addSubNode')
+            ->once()
+            ->with('scripts', 'auto-scripts', ['list' => 'cerebro-cmd']);
+
+        $this->jsonManipulatorMock->shouldReceive('getContents')
+            ->andReturn(ComposerJsonFactory::arrayToJson($composerRootJsonData));
+
+        $this->configurator->unconfigure($packageMock);
+
+        \unlink($composerJsonPath);
+    }
+}
diff --git a/tests/Automatic/Configurator/ComposerScriptsConfiguratorTest.php b/tests/Automatic/Configurator/ComposerScriptsConfiguratorTest.php
index 9ad79646..463afc7e 100644
--- a/tests/Automatic/Configurator/ComposerScriptsConfiguratorTest.php
+++ b/tests/Automatic/Configurator/ComposerScriptsConfiguratorTest.php
@@ -6,9 +6,12 @@
 use Composer\IO\IOInterface;
 use Composer\Json\JsonFile;
 use Composer\Json\JsonManipulator;
-use Narrowspark\Automatic\Common\Contract\Package as PackageContract;
+use Narrowspark\Automatic\Common\Contract\Configurator as ConfiguratorContract;
+use Narrowspark\Automatic\Common\Package;
 use Narrowspark\Automatic\Common\Traits\GetGenericPropertyReaderTrait;
+use Narrowspark\Automatic\Common\Util;
 use Narrowspark\Automatic\Configurator\ComposerScriptsConfigurator;
+use Narrowspark\Automatic\QuestionFactory;
 use Narrowspark\Automatic\Test\Fixture\ComposerJsonFactory;
 use Narrowspark\TestingHelper\Phpunit\MockeryTestCase;
 
@@ -75,69 +78,245 @@ public function testGetName(): void
 
     public function testConfigure(): void
     {
-        $composerRootJsonString = ComposerJsonFactory::createComposerScriptJson('configure', ['auto-scripts' => []]);
+        $composerRootJsonString = ComposerJsonFactory::createAutomaticComposerJson('stub/stub');
         $composerRootJsonData   = ComposerJsonFactory::jsonToArray($composerRootJsonString);
 
-        $script = ['php -v' => 'script'];
+        $package = new Package('Stub/stub', '1.0.0');
+        $package->setConfig([
+            ConfiguratorContract::TYPE => [
+                ComposerScriptsConfigurator::getName() => [
+                    'post-autoload-dump' => ['Foo\\Bar'],
+                ],
+            ],
+        ]);
 
-        $packageMock = $this->mock(PackageContract::class);
-        $packageMock->shouldReceive('getConfig')
+        $this->jsonMock->shouldReceive('read')
+            ->andReturn($composerRootJsonData);
+
+        $composerJsonPath = __DIR__ . '/composer.json';
+        \file_put_contents($composerJsonPath, \json_encode(['extra' => []]));
+
+        $this->jsonMock->shouldReceive('getPath')
             ->once()
-            ->with(ComposerScriptsConfigurator::getName())
-            ->andReturn($script);
+            ->andReturn($composerJsonPath);
+
+        $whitelist = ['composer-script-whitelist' => [$package->getName()]];
+
+        $this->jsonManipulatorMock->shouldReceive('addSubNode')
+            ->once()
+            ->with('extra', Util::COMPOSER_EXTRA_KEY, $whitelist);
+
+        $this->jsonManipulatorMock->shouldReceive('addMainKey')
+            ->once()
+            ->with('scripts', ['post-autoload-dump' => ['Foo\\Bar']]);
+
+        $composerRootJsonData['scripts']['auto-scripts'] = $whitelist;
+
+        $this->jsonManipulatorMock->shouldReceive('getContents')
+            ->andReturn(ComposerJsonFactory::arrayToJson($composerRootJsonData));
+
+        $this->ioMock->shouldReceive('askConfirmation')
+            ->once()
+            ->with(QuestionFactory::getPackageScriptsQuestion($package->getPrettyName()), false)
+            ->andReturn(true);
+
+        $this->configurator->configure($package);
+
+        \unlink($composerJsonPath);
+    }
+
+    public function testConfigureWithUpdate(): void
+    {
+        $oldWhitelist = ['composer-script-whitelist' => ['stub/stub']];
+
+        $composerRootJsonString = ComposerJsonFactory::createAutomaticComposerJson('stub/stub', [], [], $oldWhitelist);
+        $composerRootJsonData   = ComposerJsonFactory::jsonToArray($composerRootJsonString);
+
+        $package = new Package('Stub/stub', '1.0.0');
+        $package->setConfig([
+            ConfiguratorContract::TYPE => [
+                ComposerScriptsConfigurator::getName() => [
+                    'post-autoload-dump' => ['Foo\\Bar'],
+                ],
+            ],
+        ]);
 
         $this->jsonMock->shouldReceive('read')
             ->andReturn($composerRootJsonData);
 
         $composerJsonPath = __DIR__ . '/composer.json';
 
+        \file_put_contents($composerJsonPath, \json_encode(['extra' => []]));
+
         $this->jsonMock->shouldReceive('getPath')
             ->once()
             ->andReturn($composerJsonPath);
 
+        $whitelist = ['composer-script-whitelist' => [$package->getName()]];
+
         $this->jsonManipulatorMock->shouldReceive('addSubNode')
             ->once()
-            ->with('scripts', 'auto-scripts', $script);
+            ->with('extra', Util::COMPOSER_EXTRA_KEY, $whitelist);
+
+        $this->jsonManipulatorMock->shouldReceive('addMainKey')
+            ->once()
+            ->with('scripts', ['post-autoload-dump' => ['Foo\\Bar']]);
 
-        $composerRootJsonData['scripts']['auto-scripts'] = $script;
+        $composerRootJsonData['scripts']['auto-scripts'] = $whitelist;
 
         $this->jsonManipulatorMock->shouldReceive('getContents')
             ->andReturn(ComposerJsonFactory::arrayToJson($composerRootJsonData));
 
-        $this->configurator->configure($packageMock);
+        $this->ioMock->shouldReceive('askConfirmation')
+            ->never();
+
+        $this->configurator->configure($package);
 
         \unlink($composerJsonPath);
     }
 
-    public function testUnconfigure(): void
+    public function testConfigureWithBlacklist(): void
     {
-        $composerRootJsonString = ComposerJsonFactory::createComposerScriptJson('unconfigure', ['auto-scripts' => ['php -v' => 'script', 'list' => 'cerebro-cmd']]);
+        $blackList = ['composer-script-blacklist' => ['stub/stub']];
+
+        $composerRootJsonString = ComposerJsonFactory::createAutomaticComposerJson('stub/stub', [], [], $blackList);
         $composerRootJsonData   = ComposerJsonFactory::jsonToArray($composerRootJsonString);
 
-        $packageMock = $this->mock(PackageContract::class);
-        $packageMock->shouldReceive('getConfig')
+        $package = new Package('Stub/stub', '1.0.0');
+        $package->setConfig([
+            ConfiguratorContract::TYPE => [
+                ComposerScriptsConfigurator::getName() => [
+                    'post-autoload-dump' => ['Foo\\Bar'],
+                ],
+            ],
+        ]);
+
+        $this->jsonMock->shouldReceive('read')
+            ->andReturn($composerRootJsonData);
+        $this->ioMock->shouldReceive('write')
             ->once()
-            ->with(ComposerScriptsConfigurator::getName())
-            ->andReturn(['php -v' => 'script']);
+            ->with('Composer scripts for [Stub/stub] skipped, because it was found in the [composer-script-blacklist]');
+
+        $this->configurator->configure($package);
+    }
+
+    public function testConfigureNotAllowedScripts(): void
+    {
+        $composerRootJsonString = ComposerJsonFactory::createAutomaticComposerJson('stub/stub');
+        $composerRootJsonData   = ComposerJsonFactory::jsonToArray($composerRootJsonString);
+
+        $package = new Package('Stub/stub', '1.0.0');
+        $package->setConfig([
+            ConfiguratorContract::TYPE => [
+                ComposerScriptsConfigurator::getName() => [
+                    'post-autoload-dump' => ['Foo\\Bar'],
+                    'notallowed'         => 'foo',
+                ],
+            ],
+        ]);
 
         $this->jsonMock->shouldReceive('read')
             ->andReturn($composerRootJsonData);
 
         $composerJsonPath = __DIR__ . '/composer.json';
+        \file_put_contents($composerJsonPath, \json_encode(['extra' => []]));
 
         $this->jsonMock->shouldReceive('getPath')
             ->once()
             ->andReturn($composerJsonPath);
 
+        $whitelist = ['composer-script-whitelist' => [$package->getName()]];
+
         $this->jsonManipulatorMock->shouldReceive('addSubNode')
             ->once()
-            ->with('scripts', 'auto-scripts', ['list' => 'cerebro-cmd']);
+            ->with('extra', Util::COMPOSER_EXTRA_KEY, $whitelist);
+
+        $this->jsonManipulatorMock->shouldReceive('addMainKey')
+            ->once()
+            ->with('scripts', ['post-autoload-dump' => ['Foo\\Bar']]);
+
+        $composerRootJsonData['scripts']['auto-scripts'] = $whitelist;
+
+        $this->jsonManipulatorMock->shouldReceive('getContents')
+            ->andReturn(ComposerJsonFactory::arrayToJson($composerRootJsonData));
+
+        $this->ioMock->shouldReceive('askConfirmation')
+            ->once()
+            ->with(QuestionFactory::getPackageScriptsQuestion($package->getPrettyName()), false)
+            ->andReturn(true);
+
+        $this->ioMock->shouldReceive('write')
+            ->once()
+            ->with(\sprintf(
+                '<warning>    Found not allowed composer events [notallowed] in [%s]</>' . \PHP_EOL,
+                $package->getName()
+            ))
+            ->andReturn(true);
+
+        $this->configurator->configure($package);
+
+        \unlink($composerJsonPath);
+    }
+
+    public function testConfigureWithoutScripts(): void
+    {
+        $package = new Package('Stub/stub', '1.0.0');
+
+        $this->ioMock->shouldReceive('askConfirmation')
+            ->never();
+
+        $this->configurator->configure($package);
+    }
+
+    public function testUnconfigure(): void
+    {
+        $package = new Package('Stub/stub', '1.0.0');
+        $package->setConfig([
+            ConfiguratorContract::TYPE => [
+                ComposerScriptsConfigurator::getName() => [
+                    'post-autoload-dump' => ['Foo\\Bar'],
+                    'notallowed'         => 'foo',
+                ],
+            ],
+        ]);
+
+        $whitelist = ['composer-script-whitelist' => [$package->getName()]];
+
+        $composerRootJsonString = ComposerJsonFactory::createAutomaticComposerJson('stub/stub', [], [], $whitelist);
+        $composerRootJsonData   = ComposerJsonFactory::jsonToArray($composerRootJsonString);
+
+        $this->jsonMock->shouldReceive('read')
+            ->andReturn($composerRootJsonData);
+
+        $composerJsonPath = __DIR__ . '/composer.json';
+
+        $this->jsonMock->shouldReceive('getPath')
+            ->once()
+            ->andReturn($composerJsonPath);
+
+        $this->jsonManipulatorMock->shouldReceive('addSubNode')
+            ->once()
+            ->with('extra', Util::COMPOSER_EXTRA_KEY, ['composer-script-whitelist' => []]);
+
+        $this->jsonManipulatorMock->shouldReceive('addMainKey')
+            ->once()
+            ->with('scripts', []);
 
         $this->jsonManipulatorMock->shouldReceive('getContents')
             ->andReturn(ComposerJsonFactory::arrayToJson($composerRootJsonData));
 
-        $this->configurator->unconfigure($packageMock);
+        $this->configurator->unconfigure($package);
 
         \unlink($composerJsonPath);
     }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function assertPreConditions(): void
+    {
+        parent::assertPreConditions();
+
+        $this->allowMockingNonExistentMethods(true);
+    }
 }
diff --git a/tests/Automatic/Configurator/CopyFromPackageConfiguratorTest.php b/tests/Automatic/Configurator/CopyFromPackageConfiguratorTest.php
index 7d32529a..759d93c5 100644
--- a/tests/Automatic/Configurator/CopyFromPackageConfiguratorTest.php
+++ b/tests/Automatic/Configurator/CopyFromPackageConfiguratorTest.php
@@ -57,10 +57,10 @@ public function testCopyFileFromPackage(): void
 
         $this->ioMock->shouldReceive('writeError')
             ->once()
-            ->with(['    · Copying files'], true, IOInterface::VERBOSE);
+            ->with(['    - Copying files'], true, IOInterface::VERBOSE);
         $this->ioMock->shouldReceive('writeError')
             ->once()
-            ->with(['    · Created <fg=green>"copy_of_copy.txt"</>'], true, IOInterface::VERBOSE);
+            ->with(['    - Created <fg=green>"copy_of_copy.txt"</>'], true, IOInterface::VERBOSE);
 
         $this->configurator->configure($package);
 
@@ -79,10 +79,10 @@ public function testCopyDirWithFileFromPackage(): void
 
         $this->ioMock->shouldReceive('writeError')
             ->once()
-            ->with(['    · Copying files'], true, IOInterface::VERBOSE);
+            ->with(['    - Copying files'], true, IOInterface::VERBOSE);
         $this->ioMock->shouldReceive('writeError')
             ->once()
-            ->with(['    · Created <fg=green>"/css/style.css"</>'], true, IOInterface::VERBOSE);
+            ->with(['    - Created <fg=green>"/css/style.css"</>'], true, IOInterface::VERBOSE);
 
         $this->configurator->configure($package);
 
@@ -104,10 +104,10 @@ public function testTryCopyAFileThatIsNotFoundFromPackage(): void
 
         $this->ioMock->shouldReceive('writeError')
             ->once()
-            ->with(['    · Copying files'], true, IOInterface::VERBOSE);
+            ->with(['    - Copying files'], true, IOInterface::VERBOSE);
         $this->ioMock->shouldReceive('writeError')
             ->once()
-            ->with(['    · <fg=red>Failed to create "notfound.txt"</>; Error message: Failed to copy "' . __DIR__ . '/Stub/stub/notfound.txt" because file does not exist.'], true, IOInterface::VERBOSE);
+            ->with(['    - <fg=red>Failed to create "notfound.txt"</>; Error message: Failed to copy "' . __DIR__ . '/Stub/stub/notfound.txt" because file does not exist.'], true, IOInterface::VERBOSE);
 
         $this->configurator->configure($package);
 
@@ -124,19 +124,19 @@ public function testUnconfigureAFileFromPackage(): void
 
         $this->ioMock->shouldReceive('writeError')
             ->once()
-            ->with(['    · Copying files'], true, IOInterface::VERBOSE);
+            ->with(['    - Copying files'], true, IOInterface::VERBOSE);
         $this->ioMock->shouldReceive('writeError')
             ->once()
-            ->with(['    · Created <fg=green>"copy_of_copy.txt"</>'], true, IOInterface::VERBOSE);
+            ->with(['    - Created <fg=green>"copy_of_copy.txt"</>'], true, IOInterface::VERBOSE);
 
         $this->configurator->configure($package);
 
         $this->ioMock->shouldReceive('writeError')
             ->once()
-            ->with(['    · Removing files'], true, IOInterface::VERBOSE);
+            ->with(['    - Removing files'], true, IOInterface::VERBOSE);
         $this->ioMock->shouldReceive('writeError')
             ->once()
-            ->with(['    · Removed <fg=green>"copy_of_copy.txt"</>'], true, IOInterface::VERBOSE);
+            ->with(['    - Removed <fg=green>"copy_of_copy.txt"</>'], true, IOInterface::VERBOSE);
 
         $this->configurator->unconfigure($package);
     }
@@ -149,19 +149,19 @@ public function testUnconfigureADirWithFileFromPackage(): void
 
         $this->ioMock->shouldReceive('writeError')
             ->once()
-            ->with(['    · Copying files'], true, IOInterface::VERBOSE);
+            ->with(['    - Copying files'], true, IOInterface::VERBOSE);
         $this->ioMock->shouldReceive('writeError')
             ->once()
-            ->with(['    · Created <fg=green>"/css/style.css"</>'], true, IOInterface::VERBOSE);
+            ->with(['    - Created <fg=green>"/css/style.css"</>'], true, IOInterface::VERBOSE);
 
         $this->configurator->configure($package);
 
         $this->ioMock->shouldReceive('writeError')
             ->once()
-            ->with(['    · Removing files'], true, IOInterface::VERBOSE);
+            ->with(['    - Removing files'], true, IOInterface::VERBOSE);
         $this->ioMock->shouldReceive('writeError')
             ->once()
-            ->with(['    · Removed <fg=green>"/css/style.css"</>'], true, IOInterface::VERBOSE);
+            ->with(['    - Removed <fg=green>"/css/style.css"</>'], true, IOInterface::VERBOSE);
 
         $this->configurator->unconfigure($package);
 
@@ -180,10 +180,10 @@ public function testUnconfigureWithAIOException(): void
 
         $this->ioMock->shouldReceive('writeError')
             ->once()
-            ->with(['    · Copying files'], true, IOInterface::VERBOSE);
+            ->with(['    - Copying files'], true, IOInterface::VERBOSE);
         $this->ioMock->shouldReceive('writeError')
             ->once()
-            ->with(['    · Created <fg=green>"/css/style.css"</>'], true, IOInterface::VERBOSE);
+            ->with(['    - Created <fg=green>"/css/style.css"</>'], true, IOInterface::VERBOSE);
 
         $this->configurator->configure($package);
 
@@ -197,10 +197,10 @@ public function testUnconfigureWithAIOException(): void
 
         $this->ioMock->shouldReceive('writeError')
             ->once()
-            ->with(['    · Removing files'], true, IOInterface::VERBOSE);
+            ->with(['    - Removing files'], true, IOInterface::VERBOSE);
         $this->ioMock->shouldReceive('writeError')
             ->once()
-            ->with(['    · <fg=red>Failed to remove "/css/style.css"</>; Error message: '], true, IOInterface::VERBOSE);
+            ->with(['    - <fg=red>Failed to remove "/css/style.css"</>; Error message: '], true, IOInterface::VERBOSE);
 
         $this->configurator->unconfigure($package);
 
@@ -221,10 +221,10 @@ public function testCopyFileFromPackageWithConfig(): void
 
         $this->ioMock->shouldReceive('writeError')
             ->once()
-            ->with(['    · Copying files'], true, IOInterface::VERBOSE);
+            ->with(['    - Copying files'], true, IOInterface::VERBOSE);
         $this->ioMock->shouldReceive('writeError')
             ->once()
-            ->with(['    · Created <fg=green>"test/copy_of_copy.txt"</>'], true, IOInterface::VERBOSE);
+            ->with(['    - Created <fg=green>"test/copy_of_copy.txt"</>'], true, IOInterface::VERBOSE);
 
         $this->configurator->configure($package);
 
@@ -262,7 +262,7 @@ private function arrangePackageWithConfig(string $from, string $to): Package
             ->andReturn(__DIR__);
 
         $package = new Package('Stub/stub', '1.0.0');
-        $package->setConfig([ConfiguratorContract::TYPE => ['copy' => [$from => $to]]]);
+        $package->setConfig([ConfiguratorContract::TYPE => [CopyFromPackageConfigurator::getName() => [$from => $to]]]);
 
         return $package;
     }
diff --git a/tests/Automatic/Configurator/EnvConfiguratorTest.php b/tests/Automatic/Configurator/EnvConfiguratorTest.php
index 7bb5def2..d320ce14 100644
--- a/tests/Automatic/Configurator/EnvConfiguratorTest.php
+++ b/tests/Automatic/Configurator/EnvConfiguratorTest.php
@@ -78,7 +78,7 @@ public function testConfigure(): void
         $package = new Package('fixtures/test', '1.0.0');
         $package->setConfig([
             ConfiguratorContract::TYPE => [
-                'env' => [
+                EnvConfigurator::getName() => [
                     'APP_ENV'         => 'test bar',
                     'APP_DEBUG'       => '0',
                     'APP_PARAGRAPH'   => "foo\n\"bar\"\\t",
@@ -130,7 +130,7 @@ public function testUnconfigure(): void
         ];
 
         $package = new Package('fixtures/env2', '1.0.0');
-        $package->setConfig([ConfiguratorContract::TYPE => ['env' => $envConfig]]);
+        $package->setConfig([ConfiguratorContract::TYPE => [EnvConfigurator::getName() => $envConfig]]);
 
         $this->configurator->configure($package);
 
@@ -149,7 +149,7 @@ public function testUnconfigure(): void
         static::assertStringEqualsFile($this->envPath, $envContents);
 
         $package = new Package('fixtures/env2', '1.1.0');
-        $package->setConfig([ConfiguratorContract::TYPE => ['env' => $envConfig]]);
+        $package->setConfig([ConfiguratorContract::TYPE => [EnvConfigurator::getName() => $envConfig]]);
 
         $this->configurator->unconfigure($package);
 
diff --git a/tests/Automatic/Configurator/GitignoreConfiguratorTest.php b/tests/Automatic/Configurator/GitignoreConfiguratorTest.php
index 3878d312..0d8732b6 100644
--- a/tests/Automatic/Configurator/GitignoreConfiguratorTest.php
+++ b/tests/Automatic/Configurator/GitignoreConfiguratorTest.php
@@ -150,7 +150,7 @@ public function testUnconfigureWithNotFoundPackage(): void
     private function arrangePackageWithConfig(string $name, array $config): Package
     {
         $package = new Package($name, '1.0.0');
-        $package->setConfig([ConfiguratorContract::TYPE => ['gitignore' => $config]]);
+        $package->setConfig([ConfiguratorContract::TYPE => [GitIgnoreConfigurator::getName() => $config]]);
 
         return $package;
     }
diff --git a/tests/Automatic/ConfiguratorTest.php b/tests/Automatic/ConfiguratorTest.php
index e148ba5d..355861f7 100644
--- a/tests/Automatic/ConfiguratorTest.php
+++ b/tests/Automatic/ConfiguratorTest.php
@@ -41,11 +41,11 @@ public function testConfigureWithCopy(): void
 
         $this->ioMock->shouldReceive('writeError')
             ->once()
-            ->with(['    · Copying files'], true, IOInterface::VERBOSE);
+            ->with(['    - Copying files'], true, IOInterface::VERBOSE);
 
         $this->ioMock->shouldReceive('writeError')
             ->once()
-            ->with(['    · Created <fg=green>"' . $this->copyFileName . '"</>'], true, IOInterface::VERBOSE);
+            ->with(['    - Created <fg=green>"' . $this->copyFileName . '"</>'], true, IOInterface::VERBOSE);
 
         $this->configurator->configure($package);
 
@@ -56,7 +56,7 @@ public function testConfigureWithCopy(): void
 
     public function testGetConfigurators(): void
     {
-        static::assertCount(4, $this->configurator->getConfigurators());
+        static::assertCount(5, $this->configurator->getConfigurators());
     }
 
     public function testUnconfigureWithCopy(): void
@@ -67,11 +67,11 @@ public function testUnconfigureWithCopy(): void
 
         $this->ioMock->shouldReceive('writeError')
             ->once()
-            ->with(['    · Copying files'], true, IOInterface::VERBOSE);
+            ->with(['    - Copying files'], true, IOInterface::VERBOSE);
 
         $this->ioMock->shouldReceive('writeError')
             ->once()
-            ->with(['    · Created <fg=green>"' . $this->copyFileName . '"</>'], true, IOInterface::VERBOSE);
+            ->with(['    - Created <fg=green>"' . $this->copyFileName . '"</>'], true, IOInterface::VERBOSE);
 
         $this->configurator->configure($package);
 
@@ -79,11 +79,11 @@ public function testUnconfigureWithCopy(): void
 
         $this->ioMock->shouldReceive('writeError')
             ->once()
-            ->with(['    · Removing files'], true, IOInterface::VERBOSE);
+            ->with(['    - Removing files'], true, IOInterface::VERBOSE);
 
         $this->ioMock->shouldReceive('writeError')
             ->once()
-            ->with(['    · Removed <fg=green>"' . $this->copyFileName . '"</>'], true, IOInterface::VERBOSE);
+            ->with(['    - Removed <fg=green>"' . $this->copyFileName . '"</>'], true, IOInterface::VERBOSE);
 
         $this->configurator->unconfigure($package);
 
diff --git a/tests/Automatic/PackageConfiguratorTest.php b/tests/Automatic/PackageConfiguratorTest.php
index 94c1f952..6ebb483a 100644
--- a/tests/Automatic/PackageConfiguratorTest.php
+++ b/tests/Automatic/PackageConfiguratorTest.php
@@ -28,7 +28,7 @@ public function testConfiguratorWithPackageConfigurator(): void
 
         $this->ioMock->shouldReceive('writeError')
             ->twice()
-            ->with(['    · test'], true, IOInterface::VERBOSE);
+            ->with(['    - test'], true, IOInterface::VERBOSE);
 
         $this->configurator->add('mock', MockConfigurator::class);
 
@@ -51,7 +51,7 @@ public function testConfiguratorOutWithPackageConfigurator(): void
 
         $this->ioMock->shouldReceive('writeError')
             ->never()
-            ->with(['    · test'], true, IOInterface::VERBOSE);
+            ->with(['    - test'], true, IOInterface::VERBOSE);
 
         $this->configurator->configure($package);
         $this->configurator->unconfigure($package);