diff --git a/composer.json b/composer.json
index d79f8ea..baa37d4 100644
--- a/composer.json
+++ b/composer.json
@@ -20,7 +20,9 @@
"illuminate/support": "^10.3",
"league/flysystem": "^3.0",
"symfony/console": "^6.2",
- "symfony/finder": "^6.2"
+ "symfony/finder": "^6.2",
+ "symfony/process": "^6.2",
+ "vlucas/phpdotenv": "^5.5"
},
"require-dev": {
"mockery/mockery": "^1.5.1",
diff --git a/devstack b/devstack
index 7425536..3b81f35 100755
--- a/devstack
+++ b/devstack
@@ -7,13 +7,30 @@ if (file_exists($autoload = __DIR__ . '/vendor/autoload.php')) {
require dirname(__DIR__, 2) . '/autoload.php';
}
-use Symfony\Component\Console\Application;
+use Webteractive\Devstack\App;
+use Webteractive\Devstack\Commands\MySql;
+use Webteractive\Devstack\Commands\Redis;
+use Webteractive\Devstack\Commands\Shell;
use Webteractive\Devstack\Commands\Config;
-use Webteractive\Devstack\Commands\DownloadRuntimes;
use Webteractive\Devstack\Commands\InitStack;
+use Webteractive\Devstack\Commands\RunPHPCommand;
+use Webteractive\Devstack\Commands\DownloadRuntimes;
+use Webteractive\Devstack\Commands\RunComposerCommands;
+use Webteractive\Devstack\RegisterDockerComposeCommands;
+use Webteractive\Devstack\Commands\RunLaravelArtisanCommand;
+
+$app = new App('Devstack', '1.1.5');
-$app = new Application;
$app->add(new InitStack);
$app->add(new Config);
$app->add(new DownloadRuntimes);
+$app->add(new RunLaravelArtisanCommand);
+$app->add(new RunPHPCommand);
+$app->add(new RunComposerCommands);
+$app->add(new Shell);
+$app->add(new MySql);
+$app->add(new Redis);
+
+RegisterDockerComposeCommands::register($app);
+
$app->run();
\ No newline at end of file
diff --git a/src/App.php b/src/App.php
new file mode 100644
index 0000000..24f1b60
--- /dev/null
+++ b/src/App.php
@@ -0,0 +1,43 @@
+setName(static::$name);
+ $this->setVersion($version);
+ parent::__construct($name, $version);
+ }
+
+ /**
+ * @return string
+ */
+ public function getHelp(): string
+ {
+ return static::$logo . "\n\n" . parent::getHelp();
+ }
+}
diff --git a/src/CommandSignature.php b/src/CommandSignature.php
index de7b234..44ceb05 100644
--- a/src/CommandSignature.php
+++ b/src/CommandSignature.php
@@ -4,8 +4,8 @@
use Illuminate\Support\Str;
use InvalidArgumentException;
-use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Input\InputArgument;
class CommandSignature
{
diff --git a/src/Commands/Base.php b/src/Commands/Base.php
index 07472b3..b13988e 100644
--- a/src/Commands/Base.php
+++ b/src/Commands/Base.php
@@ -2,14 +2,16 @@
namespace Webteractive\Devstack\Commands;
+use Webteractive\Devstack\CommandSignature;
use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Question\Question;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
-use Symfony\Component\Console\Question\Question;
-use Webteractive\Devstack\CommandSignature;
abstract class Base extends Command
{
+ protected $fullCommandSignature;
+
protected $name;
protected $signature;
@@ -20,12 +22,12 @@ abstract class Base extends Command
protected $hidden = false;
- protected $input;
+ protected InputInterface $input;
- protected $output;
+ protected OutputInterface $output;
public function __construct()
- {
+ {
if (isset($this->signature)) {
$this->setup();
} else {
@@ -43,20 +45,32 @@ public function setup()
{
[$name, $arguments, $options] = CommandSignature::parse($this->signature);
+
+ if ($this->shouldIgnoreValidationErrors()) {
+ $this->ignoreValidationErrors();
+ }
+
parent::__construct($name);
$this->getDefinition()->addArguments($arguments);
$this->getDefinition()->addOptions($options);
}
+ public function shouldIgnoreValidationErrors(): bool
+ {
+ return false;
+ }
+
protected function execute(InputInterface $input, OutputInterface $output)
{
return $this->setIO($input, $output)
->handle();
}
- public function setIO($input, $output)
+ public function setIO(InputInterface $input, OutputInterface $output)
{
+ global $argv;
+ $this->fullCommandSignature = array_slice($argv, 2);
$this->input = $input;
$this->output = $output;
return $this;
@@ -115,6 +129,11 @@ public function line($message = '')
return $this;
}
+ public function lineBreak()
+ {
+ return $this->line('');
+ }
+
public function info($message = '')
{
$this->output->writeln(empty($message) ? '' : "{$message}");
diff --git a/src/Commands/Config.php b/src/Commands/Config.php
index 22a68b5..a043570 100644
--- a/src/Commands/Config.php
+++ b/src/Commands/Config.php
@@ -2,13 +2,7 @@
namespace Webteractive\Devstack\Commands;
-use Symfony\Component\Console\Attribute\AsCommand;
-use Symfony\Component\Console\Command\Command;
-use Symfony\Component\Console\Input\InputArgument;
-use Symfony\Component\Console\Input\InputInterface;
-use Symfony\Component\Console\Output\OutputInterface;
use Webteractive\Devstack\ShouldConfigure;
-use Webteractive\Devstack\WithStorage;
class Config extends Base
{
diff --git a/src/Commands/DownloadRuntimes.php b/src/Commands/DownloadRuntimes.php
index 6583130..6e95ac0 100644
--- a/src/Commands/DownloadRuntimes.php
+++ b/src/Commands/DownloadRuntimes.php
@@ -2,8 +2,8 @@
namespace Webteractive\Devstack\Commands;
-use Webteractive\Devstack\RuntimeDownloader;
use Webteractive\Devstack\ShouldConfigure;
+use Webteractive\Devstack\RuntimeDownloader;
class DownloadRuntimes extends Base
{
diff --git a/src/Commands/InitStack.php b/src/Commands/InitStack.php
index 6580821..16bc733 100644
--- a/src/Commands/InitStack.php
+++ b/src/Commands/InitStack.php
@@ -2,10 +2,10 @@
namespace Webteractive\Devstack\Commands;
-use Webteractive\Devstack\RuntimeDownloader;
use Webteractive\Devstack\File;
-use Webteractive\Devstack\PublicRuntimeDownloader;
use Webteractive\Devstack\ShouldConfigure;
+use Webteractive\Devstack\RuntimeDownloader;
+use Webteractive\Devstack\PublicRuntimeDownloader;
class InitStack extends Base
{
diff --git a/src/Commands/MySql.php b/src/Commands/MySql.php
new file mode 100644
index 0000000..ffdeb72
--- /dev/null
+++ b/src/Commands/MySql.php
@@ -0,0 +1,79 @@
+mysql container.';
+
+ public function handle(): int
+ {
+ $defaults = [
+ 'password' => 'password',
+ 'user' => 'user',
+ 'database' => null,
+ ];
+ $envPath = $this->input->getOption('env') ?? getcwd();
+ if (file_exists($envPath . '/.env')) {
+ $env = new Env($envPath);
+ $defaults['password'] = $env->get('DB_PASSWORD');
+ $defaults['user'] = $env->get('DB_USERNAME');
+ $defaults['database'] = $env->get('DB_DATABASE');
+ $this->lineBreak();
+ $this->line('Found an .env file in your current working directory, values will now be used as defaults.');
+ $this->lineBreak();
+ } else {
+ $this->lineBreak();
+ $this->line('Unable to find an .env file in your current working directory, now using the defaults.');
+ $this->line('If these were changed in your docker-compose.yml file, please supply it as a command');
+ $this->line('flag or add an .env file and add it there.');
+
+ $this->lineBreak();
+ $this->line('If you want to use the .env route, create a .env file and add the variables below including the values:');
+ $this->line('DB_USERNAME=');
+ $this->line('DB_PASSWORD=');
+ $this->line('DB_DATABASE=');
+ $this->lineBreak();
+ $this->line('If your .env is located somewhere else, you may use the --env=/path/to/your/.env flag.');
+ $this->line('For example, devstack mysql --env=/path/to/your/.env/directory.');
+ $this->lineBreak();
+ $this->line('Finally for the command flag, just do devstack mysql --user=the_user --password=the_password --database=the_db.');
+ $this->line('For more details on the devsack mysql command flags, run devstack help mysql.');
+ $this->lineBreak();
+ }
+
+
+ $password = $this->input->getOption('password') ?? $defaults['password'];
+ $user = $this->input->getOption('user') ?? $defaults['user'];
+ $database = $this->input->getOption('database') ?? $defaults['database'];
+
+ $bashCommand = [];
+ $bashCommand[] = "MYSQL_PWD={$password}";
+ $bashCommand[] = "mysql -u {$user}";
+ if ($database) {
+ $bashCommand[] = $database;
+ }
+
+ $this->handleTerminationSignals(
+ $process = Process::prepareFromShell('docker compose exec -it mysql bash -c "' . join(' ', $bashCommand) . '"')
+ );
+
+ $process->setTty(true)
+ ->setTimeout(60 * 60 * 2)
+ ->setIdleTimeout(60 * 60 * 8)
+ ->run();
+
+ return static::SUCCESS;
+ }
+}
\ No newline at end of file
diff --git a/src/Commands/Redis.php b/src/Commands/Redis.php
new file mode 100644
index 0000000..1162a37
--- /dev/null
+++ b/src/Commands/Redis.php
@@ -0,0 +1,29 @@
+redis container.';
+
+ public function handle(): int
+ {
+
+ $this->handleTerminationSignals(
+ $process = Process::prepareFromShell('docker compose exec -it redis redis-cli')
+ );
+
+ $process->setTty(true)
+ ->setTimeout(60 * 60 * 2)
+ ->setIdleTimeout(60 * 60 * 8)
+ ->run();
+
+ return static::SUCCESS;
+ }
+}
\ No newline at end of file
diff --git a/src/Commands/RunComposerCommands.php b/src/Commands/RunComposerCommands.php
new file mode 100644
index 0000000..54cad63
--- /dev/null
+++ b/src/Commands/RunComposerCommands.php
@@ -0,0 +1,42 @@
+handleTerminationSignals(
+ $process = Process::prepare(array_merge($command, $this->fullCommandSignature))
+ );
+
+ $process->setTty(true)->run();
+
+ return static::SUCCESS;
+ }
+}
\ No newline at end of file
diff --git a/src/Commands/RunDockerCommands.php b/src/Commands/RunDockerCommands.php
new file mode 100644
index 0000000..2c43d5d
--- /dev/null
+++ b/src/Commands/RunDockerCommands.php
@@ -0,0 +1,37 @@
+signature = $name;
+ $this->description = $description;
+
+ $this->ignoreValidationErrors();
+
+ parent::__construct();
+ }
+
+ public function handle(): int
+ {
+ $commandSignature = array_merge(
+ ['docker', 'compose', $this->getName()],
+ $this->fullCommandSignature
+ );
+
+ $this->handleTerminationSignals(
+ $process = Process::prepare($commandSignature)
+ );
+
+ $process->setTty(true)->run();
+
+ return static::SUCCESS;
+ }
+}
diff --git a/src/Commands/RunLaravelArtisanCommand.php b/src/Commands/RunLaravelArtisanCommand.php
new file mode 100644
index 0000000..4181467
--- /dev/null
+++ b/src/Commands/RunLaravelArtisanCommand.php
@@ -0,0 +1,42 @@
+handleTerminationSignals(
+ $process = Process::prepare(array_merge($command, $this->fullCommandSignature))
+ );
+
+ $process->setTty(true)->run();
+
+ return static::SUCCESS;
+ }
+}
\ No newline at end of file
diff --git a/src/Commands/RunPhpCommand.php b/src/Commands/RunPhpCommand.php
new file mode 100644
index 0000000..3cf649a
--- /dev/null
+++ b/src/Commands/RunPhpCommand.php
@@ -0,0 +1,41 @@
+handleTerminationSignals(
+ $process = Process::prepare(array_merge($command, $this->fullCommandSignature))
+ );
+
+ $process->setTty(true)->run();
+
+ return static::SUCCESS;
+ }
+}
\ No newline at end of file
diff --git a/src/Commands/Shell.php b/src/Commands/Shell.php
new file mode 100644
index 0000000..bc13adb
--- /dev/null
+++ b/src/Commands/Shell.php
@@ -0,0 +1,39 @@
+input->getOption('root')) {
+ $command[] = '-u';
+ $command[] = 'dev';
+ }
+
+ $this->handleTerminationSignals(
+ $process = Process::prepare(array_merge($command, ['app', 'bash']))
+ );
+
+ $process->setTty(true)
+ ->setTimeout(60 * 60 * 2)
+ ->setIdleTimeout(60 * 60 * 8)
+ ->run();
+
+ return static::SUCCESS;
+ }
+}
\ No newline at end of file
diff --git a/src/Env.php b/src/Env.php
new file mode 100644
index 0000000..485ef04
--- /dev/null
+++ b/src/Env.php
@@ -0,0 +1,21 @@
+dotenv = Dotenv::createImmutable($path)->safeLoad();
+ }
+
+ public function get($key, $default = null)
+ {
+ return Arr::get($this->dotenv, $key, $default);
+ }
+}
diff --git a/src/Fs.php b/src/Fs.php
index 69d2c43..25fdb89 100644
--- a/src/Fs.php
+++ b/src/Fs.php
@@ -2,7 +2,6 @@
namespace Webteractive\Devstack;
-use League\Flysystem\DirectoryListing;
use League\Flysystem\Filesystem;
use League\Flysystem\Local\LocalFilesystemAdapter;
diff --git a/src/Helpers.php b/src/Helpers.php
deleted file mode 100644
index f2d6880..0000000
--- a/src/Helpers.php
+++ /dev/null
@@ -1,8 +0,0 @@
- 'Build or rebuild services.',
+ 'config' => 'Parse, resolve and render compose file in canonical format.',
+ 'cp' => 'Copy files/folders between a service container and the local filesystem.',
+ 'create' => 'Creates containers for a service.',
+ 'down' => 'Stop and remove containers, networks.',
+ 'events' => 'Receive real time events from containers.',
+ 'exec' => 'Execute a command in a running container.',
+ 'images' => 'List images used by the created containers.',
+ 'kill' => 'Force stop service containers.',
+ 'logs' => 'View output from containers.',
+ 'ls' => 'List running compose projects.',
+ 'pause' => 'Pause services.',
+ 'port' => 'Print the public port for a port binding.',
+ 'ps' => 'List containers.',
+ 'pull' => 'Pull service images.',
+ 'push' => 'Push service images',
+ 'restart' => 'Restart service containers.',
+ 'rm' => 'Removes stopped service containers.',
+ 'run' => 'Run a one-off command on a service.',
+ 'start' => 'Start services.',
+ 'stop' => 'Stop services.',
+ 'top' => 'Display the running processes.',
+ 'unpause' => 'Unpause services.',
+ 'up' => 'Create and start containers.',
+ 'version' => 'Show the Docker Compose version information.',
+ ];
+
+ public static function register(Application $app, $prefix = 'runtime')
+ {
+ foreach (static::$commands as $name => $description) {
+ $app->add(new RunDockerCommands($name, $description));
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/WithSignalHandlers.php b/src/WithSignalHandlers.php
new file mode 100644
index 0000000..ef48ae4
--- /dev/null
+++ b/src/WithSignalHandlers.php
@@ -0,0 +1,22 @@
+signal($signal);
+ });
+ }
+
+ public function handleTerminationSignals(Process $process)
+ {
+ $this->handleSignal($process, SIGINT);
+ // $this->handleSignal($process, SIGSTOP);
+ // $this->handleSignal($process, SIGKILL);
+ }
+}
\ No newline at end of file