mirror of
https://github.com/vitodeploy/vito.git
synced 2025-07-05 07:52:34 +00:00
36
app/Console/Commands/Plugins/InstallPluginCommand.php
Normal file
36
app/Console/Commands/Plugins/InstallPluginCommand.php
Normal file
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands\Plugins;
|
||||
|
||||
use App\Facades\Plugins;
|
||||
use Exception;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class InstallPluginCommand extends Command
|
||||
{
|
||||
protected $signature = 'plugins:install {url} {--branch=} {--tag=}';
|
||||
|
||||
protected $description = 'Install a plugin from a repository';
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
$url = $this->argument('url');
|
||||
$branch = $this->option('branch');
|
||||
$tag = $this->option('tag');
|
||||
|
||||
$this->info('Installing plugin from '.$url);
|
||||
|
||||
try {
|
||||
Plugins::install($url, $branch, $tag);
|
||||
} catch (Exception $e) {
|
||||
$this->output->error($e->getMessage());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->info('Plugin installed successfully');
|
||||
}
|
||||
}
|
34
app/Console/Commands/Plugins/LoadPluginsCommand.php
Normal file
34
app/Console/Commands/Plugins/LoadPluginsCommand.php
Normal file
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands\Plugins;
|
||||
|
||||
use App\Facades\Plugins;
|
||||
use Exception;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class LoadPluginsCommand extends Command
|
||||
{
|
||||
protected $signature = 'plugins:load';
|
||||
|
||||
protected $description = 'Load all plugins from the storage/plugins directory';
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
$this->info('Loading plugins...');
|
||||
|
||||
try {
|
||||
Plugins::load();
|
||||
} catch (Exception $e) {
|
||||
$this->output->error($e->getMessage());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Plugins::cleanup();
|
||||
|
||||
$this->info('Plugins loaded successfully.');
|
||||
}
|
||||
}
|
18
app/Console/Commands/Plugins/PluginsListCommand.php
Normal file
18
app/Console/Commands/Plugins/PluginsListCommand.php
Normal file
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands\Plugins;
|
||||
|
||||
use App\Facades\Plugins;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class PluginsListCommand extends Command
|
||||
{
|
||||
protected $signature = 'plugins:list';
|
||||
|
||||
protected $description = 'List all installed plugins';
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
$this->table(['Name', 'Version'], Plugins::all());
|
||||
}
|
||||
}
|
29
app/Console/Commands/Plugins/UninstallPluginCommand.php
Normal file
29
app/Console/Commands/Plugins/UninstallPluginCommand.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands\Plugins;
|
||||
|
||||
use App\Facades\Plugins;
|
||||
use Exception;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class UninstallPluginCommand extends Command
|
||||
{
|
||||
protected $signature = 'plugins:uninstall {name}';
|
||||
|
||||
protected $description = 'Uninstall a plugin by name';
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
$this->info('Uninstalling '.$this->argument('name').'...');
|
||||
|
||||
try {
|
||||
Plugins::uninstall($this->argument('name'));
|
||||
} catch (Exception $e) {
|
||||
$this->output->error($e->getMessage());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->info('Plugin uninstalled successfully.');
|
||||
}
|
||||
}
|
20
app/Facades/Plugins.php
Normal file
20
app/Facades/Plugins.php
Normal file
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Facades;
|
||||
|
||||
use Illuminate\Support\Facades\Facade;
|
||||
|
||||
/**
|
||||
* @method static mixed all()
|
||||
* @method static string install(string $url, ?string $branch = null, ?string $tag = null)
|
||||
* @method static string load()
|
||||
* @method static string uninstall(string $name)
|
||||
* @method static void cleanup()
|
||||
*/
|
||||
class Plugins extends Facade
|
||||
{
|
||||
protected static function getFacadeAccessor(): string
|
||||
{
|
||||
return 'plugins';
|
||||
}
|
||||
}
|
89
app/Http/Controllers/PluginController.php
Normal file
89
app/Http/Controllers/PluginController.php
Normal file
@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Facades\Plugins;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
use Spatie\RouteAttributes\Attributes\Delete;
|
||||
use Spatie\RouteAttributes\Attributes\Get;
|
||||
use Spatie\RouteAttributes\Attributes\Middleware;
|
||||
use Spatie\RouteAttributes\Attributes\Post;
|
||||
use Spatie\RouteAttributes\Attributes\Prefix;
|
||||
use Throwable;
|
||||
|
||||
#[Prefix('settings/plugins')]
|
||||
#[Middleware(['auth', 'must-be-admin'])]
|
||||
class PluginController extends Controller
|
||||
{
|
||||
#[Get('/', name: 'plugins')]
|
||||
public function index(): Response
|
||||
{
|
||||
$plugins = [];
|
||||
try {
|
||||
$plugins = Plugins::all();
|
||||
} catch (Throwable $e) {
|
||||
report($e);
|
||||
}
|
||||
|
||||
return Inertia::render('plugins/index', [
|
||||
'plugins' => $plugins,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Post('/install', name: 'plugins.install')]
|
||||
public function install(Request $request): RedirectResponse
|
||||
{
|
||||
$this->validate($request, [
|
||||
'url' => 'required|url',
|
||||
]);
|
||||
|
||||
if (! composer_path() || ! php_path()) {
|
||||
return back()->with('error', 'Use CLI to install plugins.');
|
||||
}
|
||||
|
||||
$url = $request->input('url');
|
||||
|
||||
dispatch(function () use ($url) {
|
||||
try {
|
||||
Plugins::install($url);
|
||||
} catch (Throwable $e) {
|
||||
//
|
||||
}
|
||||
|
||||
Plugins::cleanup();
|
||||
})
|
||||
->onConnection('default');
|
||||
|
||||
return back()->with('info', 'Plugin is being installed...');
|
||||
}
|
||||
|
||||
#[Delete('/uninstall', name: 'plugins.uninstall')]
|
||||
public function uninstall(Request $request): RedirectResponse
|
||||
{
|
||||
$this->validate($request, [
|
||||
'name' => 'required|string',
|
||||
]);
|
||||
|
||||
if (! composer_path() || ! php_path()) {
|
||||
return back()->with('error', 'Use CLI to uninstall plugins.');
|
||||
}
|
||||
|
||||
$name = $request->input('name');
|
||||
|
||||
dispatch(function () use ($name) {
|
||||
try {
|
||||
Plugins::uninstall($name);
|
||||
} catch (Throwable) {
|
||||
//
|
||||
}
|
||||
|
||||
Plugins::cleanup();
|
||||
})
|
||||
->onConnection('default');
|
||||
|
||||
return back()->with('warning', 'Plugin is being uninstalled...');
|
||||
}
|
||||
}
|
@ -17,7 +17,7 @@
|
||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||
use ZipArchive;
|
||||
|
||||
#[Prefix('vito')]
|
||||
#[Prefix('settings/vito')]
|
||||
#[Middleware(['auth', 'must-be-admin'])]
|
||||
class VitoSettingController extends Controller
|
||||
{
|
||||
|
151
app/Plugins/Plugins.php
Normal file
151
app/Plugins/Plugins.php
Normal file
@ -0,0 +1,151 @@
|
||||
<?php
|
||||
|
||||
namespace App\Plugins;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Contracts\Filesystem\FileNotFoundException;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
|
||||
class Plugins
|
||||
{
|
||||
/**
|
||||
* @return array<array<string, string>>
|
||||
*
|
||||
* @throws FileNotFoundException
|
||||
*/
|
||||
public function all(): array
|
||||
{
|
||||
$plugins = [];
|
||||
|
||||
foreach (File::directories(plugins_path()) as $vendorDir) {
|
||||
foreach (File::directories($vendorDir) as $pluginDir) {
|
||||
$pluginComposer = $pluginDir.'/composer.json';
|
||||
if (File::exists($pluginComposer)) {
|
||||
$json = json_decode(File::get($pluginComposer), true);
|
||||
$plugins[] = [
|
||||
'name' => $json['name'] ?? 'Unknown',
|
||||
'version' => $json['version'] ?? 'Unknown',
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $plugins;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function install(string $url, ?string $branch = null, ?string $tag = null): string
|
||||
{
|
||||
$vendor = str($url)->beforeLast('/')->afterLast('/');
|
||||
$name = str($url)->afterLast('/');
|
||||
|
||||
if (is_dir(storage_path("plugins/$vendor/$name"))) {
|
||||
File::deleteDirectory(storage_path("plugins/$vendor/$name"));
|
||||
}
|
||||
|
||||
$command = "git clone $url ".storage_path("plugins/$vendor/$name");
|
||||
if ($branch) {
|
||||
$command .= " --branch $branch";
|
||||
}
|
||||
if ($tag) {
|
||||
$command .= " --tag $tag";
|
||||
}
|
||||
$command .= ' --single-branch';
|
||||
$result = Process::timeout(0)->run($command);
|
||||
$output = $result->output();
|
||||
|
||||
if ($result->failed()) {
|
||||
throw new Exception($output);
|
||||
}
|
||||
|
||||
$output .= $this->load();
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function load(): string
|
||||
{
|
||||
$storagePath = storage_path('plugins');
|
||||
$composerJson = base_path('composer.json');
|
||||
$composerLock = base_path('composer.lock');
|
||||
|
||||
// Backup composer files
|
||||
File::copy($composerJson, $composerJson.'.bak');
|
||||
File::copy($composerLock, $composerLock.'.bak');
|
||||
|
||||
$output = '';
|
||||
|
||||
foreach (File::directories($storagePath) as $vendorDir) {
|
||||
foreach (File::directories($vendorDir) as $pluginDir) {
|
||||
$pluginComposer = $pluginDir.'/composer.json';
|
||||
if (File::exists($pluginComposer)) {
|
||||
$json = json_decode(File::get($pluginComposer), true);
|
||||
if (isset($json['name'])) {
|
||||
$name = $json['name'];
|
||||
// name must be in vendor/plugin format
|
||||
if (! str_contains($name, '/')) {
|
||||
continue;
|
||||
}
|
||||
$phpPath = php_path();
|
||||
$composerPath = composer_path();
|
||||
$result = Process::timeout(0)
|
||||
->env(['PATH' => dirname($phpPath).':'.dirname($composerPath)])
|
||||
->path(base_path())
|
||||
->run(composer_path()." require $name");
|
||||
if ($result->failed()) {
|
||||
throw new Exception($result->errorOutput());
|
||||
}
|
||||
|
||||
$output .= $result->output();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function uninstall(string $name): string
|
||||
{
|
||||
$pluginPath = storage_path('plugins/'.$name);
|
||||
|
||||
if (! File::exists($pluginPath)) {
|
||||
throw new Exception("Plugin not found: $name");
|
||||
}
|
||||
|
||||
$result = Process::timeout(0)
|
||||
->path(base_path())
|
||||
->run("composer remove $name");
|
||||
|
||||
if ($result->failed()) {
|
||||
throw new Exception($result->output());
|
||||
}
|
||||
|
||||
File::deleteDirectory($pluginPath);
|
||||
|
||||
return $result->output();
|
||||
}
|
||||
|
||||
public function cleanup(): void
|
||||
{
|
||||
$composerJson = base_path('composer.json');
|
||||
$composerLock = base_path('composer.lock');
|
||||
|
||||
if (File::exists($composerJson.'.bak')) {
|
||||
File::move($composerJson.'.bak', $composerJson);
|
||||
}
|
||||
|
||||
if (File::exists($composerLock.'.bak')) {
|
||||
File::move($composerLock.'.bak', $composerLock);
|
||||
}
|
||||
}
|
||||
}
|
@ -6,6 +6,7 @@
|
||||
use App\Helpers\Notifier;
|
||||
use App\Helpers\SSH;
|
||||
use App\Models\PersonalAccessToken;
|
||||
use App\Plugins\Plugins;
|
||||
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Laravel\Fortify\Fortify;
|
||||
@ -29,6 +30,7 @@ public function boot(): void
|
||||
$this->app->bind('ssh', fn (): SSH => new SSH);
|
||||
$this->app->bind('notifier', fn (): Notifier => new Notifier);
|
||||
$this->app->bind('ftp', fn (): FTP => new FTP);
|
||||
$this->app->bind('plugins', fn (): Plugins => new Plugins);
|
||||
|
||||
Sanctum::usePersonalAccessTokenModel(PersonalAccessToken::class);
|
||||
}
|
||||
|
30
app/Providers/PluginsServiceProvider.php
Normal file
30
app/Providers/PluginsServiceProvider.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Console\Commands\Plugins\InstallPluginCommand;
|
||||
use App\Console\Commands\Plugins\LoadPluginsCommand;
|
||||
use App\Console\Commands\Plugins\PluginsListCommand;
|
||||
use App\Plugins\Plugins;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class PluginsServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function register(): void
|
||||
{
|
||||
$this->app->bind('plugins', function () {
|
||||
return new Plugins;
|
||||
});
|
||||
}
|
||||
|
||||
public function boot(): void
|
||||
{
|
||||
if ($this->app->runningInConsole()) {
|
||||
$this->commands([
|
||||
InstallPluginCommand::class,
|
||||
LoadPluginsCommand::class,
|
||||
PluginsListCommand::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
@ -5,11 +5,7 @@
|
||||
use App\DTOs\DynamicField;
|
||||
use App\DTOs\DynamicForm;
|
||||
use App\Enums\LoadBalancerMethod;
|
||||
use App\Plugins\RegisterSiteFeature;
|
||||
use App\Plugins\RegisterSiteFeatureAction;
|
||||
use App\Plugins\RegisterSiteType;
|
||||
use App\SiteFeatures\LaravelOctane\Disable;
|
||||
use App\SiteFeatures\LaravelOctane\Enable;
|
||||
use App\SiteTypes\Laravel;
|
||||
use App\SiteTypes\LoadBalancer;
|
||||
use App\SiteTypes\PHPBlank;
|
||||
@ -114,29 +110,6 @@ private function laravel(): void
|
||||
->default(false),
|
||||
]))
|
||||
->register();
|
||||
RegisterSiteFeature::make('laravel', 'laravel-octane')
|
||||
->label('Laravel Octane')
|
||||
->description('Enable Laravel Octane for this site')
|
||||
->register();
|
||||
RegisterSiteFeatureAction::make('laravel', 'laravel-octane', 'enable')
|
||||
->label('Enable')
|
||||
->form(DynamicForm::make([
|
||||
DynamicField::make('alert')
|
||||
->alert()
|
||||
->label('Alert')
|
||||
->description('Make sure you have already set the `OCTANE_SERVER` in your `.env` file'),
|
||||
DynamicField::make('port')
|
||||
->text()
|
||||
->label('Octane Port')
|
||||
->default(8000)
|
||||
->description('The port on which Laravel Octane will run.'),
|
||||
]))
|
||||
->handler(Enable::class)
|
||||
->register();
|
||||
RegisterSiteFeatureAction::make('laravel', 'laravel-octane', 'disable')
|
||||
->label('Disable')
|
||||
->handler(Disable::class)
|
||||
->register();
|
||||
}
|
||||
|
||||
public function loadBalancer(): void
|
||||
|
@ -1,62 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\SiteFeatures\LaravelOctane;
|
||||
|
||||
use App\Actions\Worker\DeleteWorker;
|
||||
use App\DTOs\DynamicField;
|
||||
use App\DTOs\DynamicForm;
|
||||
use App\Models\Worker;
|
||||
use App\SiteFeatures\Action;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class Disable extends Action
|
||||
{
|
||||
public function name(): string
|
||||
{
|
||||
return 'Disable';
|
||||
}
|
||||
|
||||
public function active(): bool
|
||||
{
|
||||
return data_get($this->site->type_data, 'octane', false);
|
||||
}
|
||||
|
||||
public function form(): ?DynamicForm
|
||||
{
|
||||
return DynamicForm::make([
|
||||
DynamicField::make('port')
|
||||
->text()
|
||||
->default(8000),
|
||||
]);
|
||||
}
|
||||
|
||||
public function handle(Request $request): void
|
||||
{
|
||||
$typeData = $this->site->type_data ?? [];
|
||||
|
||||
/** @var ?Worker $worker */
|
||||
$worker = $this->site->workers()->where('name', 'laravel-octane')->first();
|
||||
if ($worker) {
|
||||
app(DeleteWorker::class)->delete($worker);
|
||||
}
|
||||
|
||||
data_set($typeData, 'octane', false);
|
||||
data_set($typeData, 'octane_port', $request->input('port'));
|
||||
$this->site->type_data = $typeData;
|
||||
$this->site->save();
|
||||
|
||||
$webserver = $this->site->webserver()->id();
|
||||
|
||||
if ($webserver === 'nginx') {
|
||||
$this->site->webserver()->updateVHost(
|
||||
$this->site,
|
||||
replace: [
|
||||
'laravel-octane-map' => '',
|
||||
'laravel-octane' => view('ssh.services.webserver.nginx.vhost-blocks.php', ['site' => $this->site]),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
$request->session()->flash('success', 'Laravel Octane has been disabled for this site.');
|
||||
}
|
||||
}
|
@ -1,109 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\SiteFeatures\LaravelOctane;
|
||||
|
||||
use App\Actions\Worker\CreateWorker;
|
||||
use App\Actions\Worker\ManageWorker;
|
||||
use App\DTOs\DynamicField;
|
||||
use App\DTOs\DynamicForm;
|
||||
use App\Exceptions\SSHError;
|
||||
use App\Models\Worker;
|
||||
use App\SiteFeatures\Action;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use RuntimeException;
|
||||
|
||||
class Enable extends Action
|
||||
{
|
||||
public function name(): string
|
||||
{
|
||||
return 'Enable';
|
||||
}
|
||||
|
||||
public function active(): bool
|
||||
{
|
||||
return ! data_get($this->site->type_data, 'octane', false);
|
||||
}
|
||||
|
||||
public function form(): ?DynamicForm
|
||||
{
|
||||
return DynamicForm::make([
|
||||
DynamicField::make('port')
|
||||
->text()
|
||||
->default(8000),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws SSHError
|
||||
*/
|
||||
public function handle(Request $request): void
|
||||
{
|
||||
Validator::make($request->all(), [
|
||||
'port' => 'required|integer|min:1|max:65535',
|
||||
])->validate();
|
||||
|
||||
$this->site->server->ssh()->exec(
|
||||
__('php :path/artisan octane:install --no-interaction', [
|
||||
'path' => $this->site->path,
|
||||
]),
|
||||
'install-laravel-octane',
|
||||
);
|
||||
|
||||
$command = __('php :path/artisan octane:start --port=:port --host=127.0.0.1', [
|
||||
'path' => $this->site->path,
|
||||
'port' => $request->input('port'),
|
||||
]);
|
||||
|
||||
/** @var ?Worker $worker */
|
||||
$worker = $this->site->workers()->where('name', 'laravel-octane')->first();
|
||||
if ($worker) {
|
||||
app(ManageWorker::class)->restart($worker);
|
||||
} else {
|
||||
app(CreateWorker::class)->create(
|
||||
$this->site->server,
|
||||
[
|
||||
'name' => 'laravel-octane',
|
||||
'command' => $command,
|
||||
'user' => $this->site->user ?? $this->site->server->getSshUser(),
|
||||
'auto_start' => true,
|
||||
'auto_restart' => true,
|
||||
'numprocs' => 1,
|
||||
],
|
||||
$this->site,
|
||||
);
|
||||
}
|
||||
|
||||
$typeData = $this->site->type_data ?? [];
|
||||
data_set($typeData, 'octane', true);
|
||||
data_set($typeData, 'octane_port', $request->input('port'));
|
||||
$this->site->type_data = $typeData;
|
||||
$this->site->save();
|
||||
|
||||
$this->updateVHost();
|
||||
|
||||
$request->session()->flash('success', 'Laravel Octane has been enabled for this site.');
|
||||
}
|
||||
|
||||
private function updateVHost(): void
|
||||
{
|
||||
$webserver = $this->site->webserver();
|
||||
|
||||
if ($webserver->id() === 'nginx') {
|
||||
$this->site->webserver()->updateVHost(
|
||||
$this->site,
|
||||
replace: [
|
||||
'php' => view('ssh.services.webserver.nginx.vhost-blocks.laravel-octane', ['site' => $this->site]),
|
||||
'laravel-octane-map' => '',
|
||||
],
|
||||
append: [
|
||||
'header' => view('ssh.services.webserver.nginx.vhost-blocks.laravel-octane-map', ['site' => $this->site]),
|
||||
]
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
throw new RuntimeException('Unsupported webserver: '.$webserver->id());
|
||||
}
|
||||
}
|
@ -227,3 +227,41 @@ function user(): User
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
function plugins_path(?string $path = null): string
|
||||
{
|
||||
if ($path === null) {
|
||||
$path = storage_path('plugins');
|
||||
if (! file_exists($path)) {
|
||||
mkdir($path, 0755, true);
|
||||
}
|
||||
|
||||
return $path;
|
||||
}
|
||||
|
||||
return storage_path('plugins'.'/'.$path);
|
||||
}
|
||||
|
||||
function composer_path(): ?string
|
||||
{
|
||||
$paths = [
|
||||
'/usr/local/bin/composer',
|
||||
'/usr/bin/composer',
|
||||
'/opt/homebrew/bin/composer',
|
||||
trim((string) shell_exec('which composer')),
|
||||
];
|
||||
|
||||
return array_find($paths, fn ($path) => is_executable($path));
|
||||
}
|
||||
|
||||
function php_path(): ?string
|
||||
{
|
||||
$paths = [
|
||||
'/usr/local/bin/php',
|
||||
'/usr/bin/php',
|
||||
'/opt/homebrew/bin/php',
|
||||
trim((string) shell_exec('which php')),
|
||||
];
|
||||
|
||||
return array_find($paths, fn ($path) => is_executable($path));
|
||||
}
|
||||
|
Reference in New Issue
Block a user