Compare commits

..

33 Commits
2.2.0 ... cli

Author SHA1 Message Date
9d4c7972ed Merge branch 'cli' of github.com:vitodeploy/vito into cli 2025-03-02 10:59:56 +01:00
abe27cde01 wip 2025-03-02 10:59:46 +01:00
49137a757b Fix console working directory for root user (#525)
Previously, `$(pwd)` was expanded too early by the parent shell, leading to incorrect working directory output when `cd` was executed within `$request->command`. Replaced `echo "VITO_WORKING_DIR: $(pwd)"` with `echo -n "VITO_WORKING_DIR: " && pwd` to ensure `pwd` executes at the right moment inside the same shell session.

Closes #515
2025-03-02 10:55:06 +01:00
97e20206e8 ite Commands (#298) (#519)
* feat: Implement Site Commands (#298)

- Introduced a Commands widget/table for Site view, allowing users to create, edit, delete, and execute commands.
- Each Site Type now has a predefined set of commands inserted upon site creation.
- A migration script ensures commands are created for existing sites.
- Implemented necessary policies for command management.
- Added feature tests to validate functionality.

* I'm trying to fix the tests, but it seems like it might not work. I'm having trouble running the tests locally for some reason.

* I'm trying to fix the tests, but it seems like it might not work. I'm having trouble running the tests locally for some reason.

* I'm trying to fix the tests, but it seems like it might not work. I'm having trouble running the tests locally for some reason.

* I'm trying to fix the tests, but it seems like it might not work. I'm having trouble running the tests locally for some reason.

* Remove feature tests for commands due to inconsistencies for now

* fixes

* add tests for commands

* ui fix and add to wordpress

---------

Co-authored-by: Saeed Vaziry <mr.saeedvaziry@gmail.com>
2025-03-02 10:43:26 +01:00
176ff3bbc4 fix: Fix variables not being saved in database when executing a script (#518) 2025-02-28 23:26:18 +01:00
b2440586d6 fix: Fix regex in File Manager to support hyphenated usernames and groups (#516) 2025-02-28 19:49:33 +01:00
4b8e798e66 fix: prevent "null" from appearing in console after user selection (#501) (#514)
Ensure a default working directory is returned when fetching the console working directory. Previously, if a user is switched before running any commands, `Cache::get` would return `null`. Now, it defaults to `'~'` if no value exists.

Closes #501.
2025-02-28 19:44:12 +01:00
6143eb94b4 feat: display repository and branch in site details (#512)
Added `repository` and `branch` fields to the site details view. These fields are now visible when a site has a corresponding repository or branch and are formatted accordingly.
2025-02-28 19:43:20 +01:00
e52903c649 fix: ensure newly created branches are available for switching (#511)
Fixed an issue where the "Change Branch" button didn't work when switching to a newly created remote branch after initially cloning the repository. Added `git fetch origin` to update branch references before switching.
2025-02-28 19:38:38 +01:00
1a5cf4c57a fix: add missing php-intl package (#510)
Added `php{{ $version }}-intl` to the installation list as it was missing. This package is a standard component in most projects and should be included by default.
2025-02-28 19:38:10 +01:00
d8ece27964 Add logs:clear command (#509) 2025-02-27 22:19:50 +01:00
f54c754971 update docker publish script 2025-02-26 21:22:59 +01:00
7cda14cb76 Bump version 2.3.0 2025-02-26 20:49:20 +01:00
3bf3f7eebc Fix filemanager permissions (#508)
* Fix filemanager permissions

* fix filemanager permissions

* fix tests warning
2025-02-26 20:46:07 +01:00
e17fdbb1a0 Bump esbuild, laravel-vite-plugin and vite (#507)
Bumps [esbuild](https://github.com/evanw/esbuild) to 0.25.0 and updates ancestor dependencies [esbuild](https://github.com/evanw/esbuild), [laravel-vite-plugin](https://github.com/laravel/vite-plugin) and [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite). These dependencies need to be updated together.


Updates `esbuild` from 0.18.20 to 0.25.0
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG-2023.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.18.20...v0.25.0)

Updates `laravel-vite-plugin` from 0.7.8 to 1.2.0
- [Release notes](https://github.com/laravel/vite-plugin/releases)
- [Changelog](https://github.com/laravel/vite-plugin/blob/1.x/CHANGELOG.md)
- [Upgrade guide](https://github.com/laravel/vite-plugin/blob/1.x/UPGRADE.md)
- [Commits](https://github.com/laravel/vite-plugin/compare/v0.7.8...v1.2.0)

Updates `vite` from 4.5.9 to 6.2.0
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/create-vite@6.2.0/packages/vite)

---
updated-dependencies:
- dependency-name: esbuild
  dependency-type: indirect
- dependency-name: laravel-vite-plugin
  dependency-type: direct:development
- dependency-name: vite
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-26 19:33:33 +01:00
e99146209e Fix auto-deployment branch (#506) 2025-02-23 12:50:46 +01:00
1223ea1499 Fix .env Files for Isolated Users (#496) 2025-02-22 09:23:03 +01:00
a1cf09e35d wip 2025-02-20 18:00:13 +01:00
2356e44f5b Fix deployment script command (#498) 2025-02-19 20:13:26 +01:00
8c7c3d2192 Refactor firewall and add edit rule (#488) 2025-02-16 20:31:58 +01:00
e2b9d18a71 Built-in File Manager (#458) 2025-02-16 19:56:21 +01:00
75e554ad74 Disable docker build action (#494) 2025-02-16 16:25:59 +01:00
4d59529767 Increase the content width (#492) 2025-02-16 15:22:44 +01:00
ea31d0978f Cache docker build 2025-02-16 13:15:02 +01:00
ee4e9e5452 Upgrade docker to Ubuntu 24.04 2025-02-16 12:38:48 +01:00
48c12e26b2 Fix docker build 2025-02-16 12:36:33 +01:00
72f68b5917 Fix docker build (#491) 2025-02-16 12:32:36 +01:00
a889cf11a2 Fix different PHP version for sites (#487) 2025-02-16 10:58:58 +01:00
75a4fde8de Update the vhost file to name the backend after the domain (#485) 2025-02-15 09:33:40 +01:00
fd67097884 Add mariadb missing blades (#476)
* Add missing views for Mariadb

* Add missing restore link

* adding test to avoid such issues

---------

Co-authored-by: Saeed Vaziry <mr.saeedvaziry@gmail.com>
2025-02-07 20:24:08 +01:00
705d029a63 bump version to 2.2.1 (#474) 2025-02-07 18:24:02 +01:00
7be63384d4 fix html especial characters in commands (#467) 2025-02-06 20:34:01 +01:00
dd78c86a60 Fix missing ip for AWS and DO (#461)
* Fix missing ip for AWS and DO

* Fix Vultr
2025-02-05 21:26:09 +01:00
128 changed files with 3769 additions and 485 deletions

View File

@ -2,9 +2,9 @@ name: Docker Latest
on: on:
workflow_dispatch: workflow_dispatch:
push: # push:
branches: # branches:
- 2.x # - 2.x
jobs: jobs:
build-and-push: build-and-push:
@ -31,5 +31,4 @@ jobs:
-f docker/Dockerfile \ -f docker/Dockerfile \
-t vitodeploy/vito:latest \ -t vitodeploy/vito:latest \
--platform linux/amd64,linux/arm64 \ --platform linux/amd64,linux/arm64 \
--no-cache \
--push --push

View File

@ -2,8 +2,8 @@ name: Docker Release
on: on:
workflow_dispatch: workflow_dispatch:
release: # release:
types: [ created ] # types: [ created ]
jobs: jobs:
build-and-push: build-and-push:

View File

@ -40,5 +40,8 @@ jobs:
- name: Create sqlite database - name: Create sqlite database
run: touch storage/database-test.sqlite run: touch storage/database-test.sqlite
- name: Set up the .env file
run: touch .env
- name: Run test suite - name: Run test suite
run: php artisan test run: php artisan test

View File

@ -15,16 +15,12 @@ class ManageBackupFile
*/ */
public function download(BackupFile $file): StreamedResponse public function download(BackupFile $file): StreamedResponse
{ {
$localFilename = "backup_{$file->id}_{$file->name}.zip"; $file->backup->server->ssh()->download(
Storage::disk('tmp')->path(basename($file->path())),
$file->path()
);
if (! Storage::disk('backups')->exists($localFilename)) { return Storage::disk('tmp')->download(basename($file->path()));
$file->backup->server->ssh()->download(
Storage::disk('backups')->path($localFilename),
$file->path()
);
}
return Storage::disk('backups')->download($localFilename, $file->name.'.zip');
} }
public function delete(BackupFile $file): void public function delete(BackupFile $file): void

View File

@ -0,0 +1,39 @@
<?php
namespace App\Actions\FileManager;
use App\Exceptions\SSHError;
use App\Models\File;
use App\Models\Server;
use App\Models\User;
use Illuminate\Validation\Rule;
class FetchFiles
{
/**
* @throws SSHError
*/
public function fetch(User $user, Server $server, array $input): void
{
File::parse(
$user,
$server,
$input['path'],
$input['user'],
$server->os()->ls($input['path'], $input['user'])
);
}
public static function rules(Server $server): array
{
return [
'path' => [
'required',
],
'user' => [
'required',
Rule::in($server->getSshUsers()),
],
];
}
}

View File

@ -1,67 +0,0 @@
<?php
namespace App\Actions\FirewallRule;
use App\Enums\FirewallRuleStatus;
use App\Models\FirewallRule;
use App\Models\Server;
use App\SSH\Services\Firewall\Firewall;
use Illuminate\Validation\Rule;
class CreateRule
{
public function create(Server $server, array $input): FirewallRule
{
$rule = new FirewallRule([
'server_id' => $server->id,
'type' => $input['type'],
'protocol' => $input['protocol'],
'port' => $input['port'],
'source' => $input['source'],
'mask' => $input['mask'] ?? null,
]);
/** @var Firewall $firewallHandler */
$firewallHandler = $server->firewall()->handler();
$firewallHandler->addRule(
$rule->type,
$rule->getRealProtocol(),
$rule->port,
$rule->source,
$rule->mask
);
$rule->status = FirewallRuleStatus::READY;
$rule->save();
return $rule;
}
public static function rules(): array
{
return [
'type' => [
'required',
'in:allow,deny',
],
'protocol' => [
'required',
Rule::in(array_keys(config('core.firewall_protocols_port'))),
],
'port' => [
'required',
'numeric',
'min:1',
'max:65535',
],
'source' => [
'required',
'ip',
],
'mask' => [
'nullable',
'numeric',
],
];
}
}

View File

@ -1,28 +0,0 @@
<?php
namespace App\Actions\FirewallRule;
use App\Enums\FirewallRuleStatus;
use App\Models\FirewallRule;
use App\Models\Server;
class DeleteRule
{
public function delete(Server $server, FirewallRule $rule): void
{
$rule->status = FirewallRuleStatus::DELETING;
$rule->save();
$server->firewall()
->handler()
->removeRule(
$rule->type,
$rule->getRealProtocol(),
$rule->port,
$rule->source,
$rule->mask
);
$rule->delete();
}
}

View File

@ -0,0 +1,117 @@
<?php
namespace App\Actions\FirewallRule;
use App\Enums\FirewallRuleStatus;
use App\Models\FirewallRule;
use App\Models\Server;
use App\SSH\Services\Firewall\Firewall;
class ManageRule
{
public function create(Server $server, array $input): FirewallRule
{
$sourceAny = $input['source_any'] ?? empty($input['source'] ?? null);
$rule = new FirewallRule([
'name' => $input['name'],
'server_id' => $server->id,
'type' => $input['type'],
'protocol' => $input['protocol'],
'port' => $input['port'],
'source' => $sourceAny ? null : $input['source'],
'mask' => $sourceAny ? null : ($input['mask'] ?? null),
'status' => FirewallRuleStatus::CREATING,
]);
$rule->save();
dispatch(fn () => $this->applyRule($rule));
return $rule;
}
public function update(FirewallRule $rule, array $input): FirewallRule
{
$sourceAny = $input['source_any'] ?? empty($input['source'] ?? null);
$rule->update([
'name' => $input['name'],
'type' => $input['type'],
'protocol' => $input['protocol'],
'port' => $input['port'],
'source' => $sourceAny ? null : $input['source'],
'mask' => $sourceAny ? null : ($input['mask'] ?? null),
'status' => FirewallRuleStatus::UPDATING,
]);
dispatch(fn () => $this->applyRule($rule));
return $rule;
}
public function delete(FirewallRule $rule): void
{
$rule->status = FirewallRuleStatus::DELETING;
$rule->save();
dispatch(fn () => $this->applyRule($rule));
}
protected function applyRule($rule): void
{
try {
/** @var Firewall $handler */
$handler = $rule->server->firewall()->handler();
$handler->applyRules();
} catch (\Exception $e) {
$rule->server->firewallRules()
->where('status', '!=', FirewallRuleStatus::READY)
->update(['status' => FirewallRuleStatus::FAILED]);
throw $e;
}
if ($rule->status === FirewallRuleStatus::DELETING) {
$rule->delete();
return;
}
$rule->status = FirewallRuleStatus::READY;
$rule->save();
}
public static function rules(): array
{
return [
'name' => [
'required',
'string',
'max:18',
],
'type' => [
'required',
'in:allow,deny',
],
'protocol' => [
'required',
'in:tcp,udp',
],
'port' => [
'required',
'numeric',
'min:1',
'max:65535',
],
'source' => [
'nullable',
'ip',
],
'mask' => [
'nullable',
'numeric',
'min:1',
'max:32',
],
];
}
}

View File

@ -86,6 +86,9 @@ private function install(Server $server): void
while ($maxWait > 0) { while ($maxWait > 0) {
sleep(10); sleep(10);
$maxWait -= 10; $maxWait -= 10;
if (! $server->provider()->isRunning()) {
continue;
}
try { try {
$server->ssh()->connect(); $server->ssh()->connect();
break; break;
@ -194,26 +197,29 @@ public function createFirewallRules(Server $server): void
$server->firewallRules()->createMany([ $server->firewallRules()->createMany([
[ [
'type' => 'allow', 'type' => 'allow',
'protocol' => 'ssh', 'name' => 'SSH',
'protocol' => 'tcp',
'port' => 22, 'port' => 22,
'source' => '0.0.0.0', 'source' => null,
'mask' => 0, 'mask' => null,
'status' => FirewallRuleStatus::READY, 'status' => FirewallRuleStatus::READY,
], ],
[ [
'type' => 'allow', 'type' => 'allow',
'protocol' => 'http', 'name' => 'HTTP',
'protocol' => 'tcp',
'port' => 80, 'port' => 80,
'source' => '0.0.0.0', 'source' => null,
'mask' => 0, 'mask' => null,
'status' => FirewallRuleStatus::READY, 'status' => FirewallRuleStatus::READY,
], ],
[ [
'type' => 'allow', 'type' => 'allow',
'protocol' => 'https', 'name' => 'HTTPS',
'protocol' => 'tcp',
'port' => 443, 'port' => 443,
'source' => '0.0.0.0', 'source' => null,
'mask' => 0, 'mask' => null,
'status' => FirewallRuleStatus::READY, 'status' => FirewallRuleStatus::READY,
], ],
]); ]);

View File

@ -0,0 +1,29 @@
<?php
namespace App\Actions\Site;
use App\Models\Command;
use App\Models\Site;
class CreateCommand
{
public function create(Site $site, array $input): Command
{
$script = new Command([
'site_id' => $site->id,
'name' => $input['name'],
'command' => $input['command'],
]);
$script->save();
return $script;
}
public static function rules(): array
{
return [
'name' => ['required', 'string', 'max:255'],
'command' => ['required', 'string'],
];
}
}

View File

@ -68,6 +68,9 @@ public function create(Server $server, array $input): Site
'content' => '', 'content' => '',
]); ]);
// create base commands if any
$site->commands()->createMany($site->type()->baseCommands());
// install site // install site
dispatch(function () use ($site) { dispatch(function () use ($site) {
$site->type()->install(); $site->type()->install();

View File

@ -0,0 +1,25 @@
<?php
namespace App\Actions\Site;
use App\Models\Command;
class EditCommand
{
public function edit(Command $command, array $input): Command
{
$command->name = $input['name'];
$command->command = $input['command'];
$command->save();
return $command;
}
public static function rules(): array
{
return [
'name' => ['required', 'string', 'max:255'],
'command' => ['required', 'string'],
];
}
}

View File

@ -0,0 +1,58 @@
<?php
namespace App\Actions\Site;
use App\Enums\CommandExecutionStatus;
use App\Models\Command;
use App\Models\CommandExecution;
use App\Models\ServerLog;
use App\Models\User;
class ExecuteCommand
{
public function execute(Command $command, User $user, array $input): CommandExecution
{
$execution = new CommandExecution([
'command_id' => $command->id,
'server_id' => $command->site->server_id,
'user_id' => $user->id,
'variables' => $input['variables'] ?? [],
'status' => CommandExecutionStatus::EXECUTING,
]);
$execution->save();
dispatch(function () use ($execution, $command) {
$content = $execution->getContent();
$log = ServerLog::make($execution->server, 'command-'.$command->id.'-'.strtotime('now'));
$log->save();
$execution->server_log_id = $log->id;
$execution->save();
$execution->server->os()->runScript(
path: $command->site->path,
script: $content,
user: $command->site->user,
serverLog: $log,
variables: $execution->variables
);
$execution->status = CommandExecutionStatus::COMPLETED;
$execution->save();
})->catch(function () use ($execution) {
$execution->status = CommandExecutionStatus::FAILED;
$execution->save();
})->onConnection('ssh');
return $execution;
}
public static function rules(array $input): array
{
return [
'variables' => 'array',
'variables.*' => [
'required',
'string',
'max:255',
],
];
}
}

View File

@ -16,6 +16,7 @@ class UpdateBranch
public function update(Site $site, array $input): void public function update(Site $site, array $input): void
{ {
$site->branch = $input['branch']; $site->branch = $input['branch'];
app(Git::class)->fetchOrigin($site);
app(Git::class)->checkout($site); app(Git::class)->checkout($site);
$site->save(); $site->save();
} }

View File

@ -2,19 +2,20 @@
namespace App\Actions\Site; namespace App\Actions\Site;
use App\Exceptions\SSHUploadFailed; use App\Exceptions\SSHError;
use App\Models\Site; use App\Models\Site;
class UpdateEnv class UpdateEnv
{ {
/** /**
* @throws SSHUploadFailed * @throws SSHError
*/ */
public function update(Site $site, array $input): void public function update(Site $site, array $input): void
{ {
$site->server->os()->editFile( $site->server->os()->editFileAs(
$site->path.'/.env', $site->path.'/.env',
$input['env'] $site->user,
trim($input['env']),
); );
} }
} }

View File

@ -0,0 +1,32 @@
<?php
namespace App\Cli\Commands;
use App\Models\User;
use Illuminate\Console\Command;
use function Laravel\Prompts\error;
abstract class AbstractCommand extends Command
{
private User|null $user = null;
public function user()
{
if ($this->user) {
return $this->user->refresh();
}
/** @var User $user */
$user = User::query()->first();
if (!$user) {
error('The application is not setup');
exit(1);
}
$this->user = $user;
return $user;
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace App\Cli\Commands;
use Illuminate\Console\Command;
class InfoCommand extends Command
{
protected $signature = 'info';
protected $description = 'Show the application information';
public function handle(): void
{
$this->info('Version: '.config('app.version'));
$this->info('Timezone: '.config('app.timezone'));
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace App\Cli\Commands\Projects;
use App\Cli\Commands\AbstractCommand;
use App\Models\Project;
use function Laravel\Prompts\table;
class ProjectsListCommand extends AbstractCommand
{
protected $signature = 'projects:list';
protected $description = 'Show projects list';
public function handle(): void
{
$projects = Project::all();
table(
headers: ['ID', 'Name', 'Selected'],
rows: $projects->map(fn (Project $project) => [
$project->id,
$project->name,
$project->id === $this->user()->current_project_id ? 'Yes' : 'No',
])->toArray(),
);
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace App\Cli\Commands\Projects;
use App\Cli\Commands\AbstractCommand;
use App\Models\Project;
use function Laravel\Prompts\error;
use function Laravel\Prompts\info;
class ProjectsSelectCommand extends AbstractCommand
{
protected $signature = 'projects:select {project}';
protected $description = 'Select a project';
public function handle(): void
{
$project = Project::query()->find($this->argument('project'));
if (! $project) {
error('The project does not exist');
return;
}
$this->user()->update([
'current_project_id' => $project->id,
]);
info(__('The project [:project] has been selected' , [
'project' => $project->name,
]));
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace App\Cli\Commands\ServerProviders;
use App\Cli\Commands\AbstractCommand;
use function Laravel\Prompts\select;
use function Laravel\Prompts\text;
class ServerProvidersCreateCommand extends AbstractCommand
{
protected $signature = 'server-providers:create';
protected $description = 'Create a new server provider';
public function handle(): void
{
$provider = select(
label: 'What is the server provider?',
options: collect(config('core.server_providers'))
->filter(fn($provider) => $provider != \App\Enums\ServerProvider::CUSTOM)
->mapWithKeys(fn($provider) => [$provider => $provider]),
);
$profile = text(
label: 'What should we call this provider profile?',
required: true,
);
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace App\Cli\Commands\ServerProviders;
use App\Cli\Commands\AbstractCommand;
use App\Models\Project;
use App\Models\Server;
use App\Models\ServerProvider;
use function Laravel\Prompts\table;
class ServerProvidersListCommand extends AbstractCommand
{
protected $signature = 'server-providers:list';
protected $description = 'Show server providers list';
public function handle(): void
{
$providers = $this->user()->serverProviders;
table(
headers: ['ID', 'Provider', 'Name', 'Created At'],
rows: $providers->map(fn (ServerProvider $provider) => [
$provider->id,
$provider->provider,
$provider->profile,
$provider->created_at_by_timezone,
])->toArray(),
);
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace App\Cli\Commands\Servers;
use App\Cli\Commands\AbstractCommand;
use function Laravel\Prompts\select;
use function Laravel\Prompts\text;
class ServersCreateCommand extends AbstractCommand
{
protected $signature = 'servers:create';
protected $description = 'Create a new server';
public function handle(): void
{
$name = text(
label: 'What is the server name?',
required: true,
);
$os = select(
label: 'What is the server OS?',
options: collect(config('core.operating_systems'))
->mapWithKeys(fn($value) => [$value => $value]),
);
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace App\Cli\Commands\Servers;
use App\Cli\Commands\AbstractCommand;
use App\Models\Project;
use App\Models\Server;
use function Laravel\Prompts\table;
class ServersListCommand extends AbstractCommand
{
protected $signature = 'servers:list';
protected $description = 'Show servers list';
public function handle(): void
{
$servers = $this->user()->currentProject->servers;
table(
headers: ['ID', 'Name', 'IP', 'Provider', 'OS', 'Status', 'Created At'],
rows: $servers->map(fn (Server $server) => [
$server->id,
$server->name,
$server->ip,
$server->provider,
$server->os,
$server->status,
$server->created_at_by_timezone,
])->toArray(),
);
}
}

View File

@ -0,0 +1,81 @@
<?php
namespace App\Cli\Commands;
use App\Models\Project;
use App\Models\User;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\File;
use function Laravel\Prompts\text;
use function Laravel\Prompts\info;
class SetupCommand extends Command
{
protected $signature = 'setup';
protected $description = 'Setup the application';
public function handle(): void
{
$this->prepareStorage();
$this->migrate();
$this->makeUser();
$this->makeProject();
info('The application has been setup');
}
private function prepareStorage(): void
{
File::ensureDirectoryExists(storage_path());
}
private function migrate(): void
{
$this->call('migrate', ['--force' => true]);
}
private function makeUser(): void
{
$user = User::query()->first();
if ($user) {
return;
}
$name = text(
label: 'What is your name?',
required: true,
);
$email = text(
label: 'What is your email?',
required: true,
);
User::query()->create([
'name' => $name,
'email' => $email,
'password' => bcrypt(str()->random(16)),
]);
}
private function makeProject(): void
{
$project = Project::query()->first();
if ($project) {
return;
}
$project = Project::query()->create([
'name' => 'default',
]);
$user = User::query()->first();
$user->update([
'current_project_id' => $project->id,
]);
}
}

43
app/Cli/Kernel.php Normal file
View File

@ -0,0 +1,43 @@
<?php
namespace App\Cli;
use Illuminate\Console\Application as Artisan;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Database\Console\Migrations\InstallCommand;
use Illuminate\Database\Console\Migrations\MigrateCommand;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
class Kernel extends ConsoleKernel
{
protected $commands = [
'command.migrate',
'command.migrate.install'
];
/**
* Register the commands for the application.
*/
protected function commands(): void
{
$this->load(__DIR__ . '/Commands');
$this->app->singleton('command.migrate', function ($app) {
return new MigrateCommand($app['migrator'], $app[Dispatcher::class]);
});
$this->app->singleton('command.migrate.install', function ($app) {
return new InstallCommand($app['migration.repository']);
});
}
protected function shouldDiscoverCommands(): false
{
return false;
}
protected function getArtisan(): ?Artisan
{
return $this->artisan = (new Artisan($this->app, $this->events, $this->app->version()))
->resolveCommands($this->commands);
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace App\Console\Commands;
use App\Models\ServerLog;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Storage;
class ClearLogsCommand extends Command
{
protected $signature = 'logs:clear';
protected $description = 'Clear all server logs';
public function handle(): void
{
$this->info('Clearing logs...');
ServerLog::query()->delete();
File::cleanDirectory(Storage::disk('server-logs')->path(''));
$this->info('Logs cleared!');
}
}

View File

@ -0,0 +1,12 @@
<?php
namespace App\Enums;
final class CommandExecutionStatus
{
const EXECUTING = 'executing';
const COMPLETED = 'completed';
const FAILED = 'failed';
}

View File

@ -6,7 +6,11 @@ final class FirewallRuleStatus
{ {
const CREATING = 'creating'; const CREATING = 'creating';
const UPDATING = 'updating';
const READY = 'ready'; const READY = 'ready';
const DELETING = 'deleting'; const DELETING = 'deleting';
const FAILED = 'failed';
} }

View File

@ -11,4 +11,6 @@ final class SiteFeature
const SSL = 'ssl'; const SSL = 'ssl';
const QUEUES = 'queues'; const QUEUES = 'queues';
const COMMANDS = 'commands';
} }

View File

@ -14,6 +14,9 @@
* @method static setLog(?ServerLog $log) * @method static setLog(?ServerLog $log)
* @method static connect() * @method static connect()
* @method static string exec(string $command, string $log = '', int $siteId = null, ?bool $stream = false, callable $streamCallback = null) * @method static string exec(string $command, string $log = '', int $siteId = null, ?bool $stream = false, callable $streamCallback = null)
* @method static string upload(string $local, string $remote, ?string $owner = null)
* @method static string download(string $local, string $remote)
* @method static string write(string $path, string $content, string $owner = null)
* @method static string assertExecuted(array|string $commands) * @method static string assertExecuted(array|string $commands)
* @method static string assertExecutedContains(string $command) * @method static string assertExecutedContains(string $command)
* @method static string assertFileUploaded(string $toPath, ?string $content = null) * @method static string assertFileUploaded(string $toPath, ?string $content = null)

View File

@ -94,11 +94,7 @@ public function connect(bool $sftp = false): void
*/ */
public function exec(string $command, string $log = '', ?int $siteId = null, ?bool $stream = false, ?callable $streamCallback = null): string public function exec(string $command, string $log = '', ?int $siteId = null, ?bool $stream = false, ?callable $streamCallback = null): string
{ {
if (! $log) { if (! $this->log && $log) {
$log = 'run-command';
}
if (! $this->log) {
$this->log = ServerLog::make($this->server, $log); $this->log = ServerLog::make($this->server, $log);
if ($siteId) { if ($siteId) {
$this->log->forSite($siteId); $this->log->forSite($siteId);
@ -116,13 +112,15 @@ public function exec(string $command, string $log = '', ?int $siteId = null, ?bo
try { try {
if ($this->asUser) { if ($this->asUser) {
$command = 'sudo su - '.$this->asUser.' -c '.'"'.addslashes($command).'"'; $command = addslashes($command);
$command = str_replace('\\\'', '\'', $command);
$command = 'sudo su - '.$this->asUser.' -c '.'"'.trim($command).'"';
} }
$this->connection->setTimeout(0); $this->connection->setTimeout(0);
if ($stream) { if ($stream) {
$this->connection->exec($command, function ($output) use ($streamCallback) { $this->connection->exec($command, function ($output) use ($streamCallback) {
$this->log->write($output); $this->log?->write($output);
return $streamCallback($output); return $streamCallback($output);
}); });
@ -131,7 +129,7 @@ public function exec(string $command, string $log = '', ?int $siteId = null, ?bo
} else { } else {
$output = ''; $output = '';
$this->connection->exec($command, function ($out) use (&$output) { $this->connection->exec($command, function ($out) use (&$output) {
$this->log->write($out); $this->log?->write($out);
$output .= $out; $output .= $out;
}); });
@ -159,7 +157,7 @@ public function exec(string $command, string $log = '', ?int $siteId = null, ?bo
/** /**
* @throws Throwable * @throws Throwable
*/ */
public function upload(string $local, string $remote): void public function upload(string $local, string $remote, ?string $owner = null): void
{ {
$this->log = null; $this->log = null;
@ -167,7 +165,17 @@ public function upload(string $local, string $remote): void
$this->connect(true); $this->connect(true);
} }
$this->connection->put($remote, $local, SFTP::SOURCE_LOCAL_FILE); $tmpName = Str::random(10).strtotime('now');
$tempPath = home_path($this->user).'/'.$tmpName;
$this->connection->put($tempPath, $local, SFTP::SOURCE_LOCAL_FILE);
$this->exec(sprintf('sudo mv %s %s', $tempPath, $remote));
if (! $owner) {
$owner = $this->user;
}
$this->exec(sprintf('sudo chown %s:%s %s', $owner, $owner, $remote));
$this->exec(sprintf('sudo chmod 644 %s', $remote));
} }
/** /**
@ -187,22 +195,15 @@ public function download(string $local, string $remote): void
/** /**
* @throws SSHError * @throws SSHError
*/ */
public function write(string $remotePath, string $content, bool $sudo = false): void public function write(string $remotePath, string $content, ?string $owner = null): void
{ {
$tmpName = Str::random(10).strtotime('now'); $tmpName = Str::random(10).strtotime('now');
try { try {
/** @var FilesystemAdapter $storageDisk */ /** @var FilesystemAdapter $storageDisk */
$storageDisk = Storage::disk('local'); $storageDisk = Storage::disk('local');
$storageDisk->put($tmpName, $content); $storageDisk->put($tmpName, $content);
$this->upload($storageDisk->path($tmpName), $remotePath, $owner);
if ($sudo) {
$this->upload($storageDisk->path($tmpName), sprintf('/home/%s/%s', $this->server->ssh_user, $tmpName));
$this->exec(sprintf('sudo mv /home/%s/%s %s', $this->server->ssh_user, $tmpName, $remotePath));
} else {
$this->upload($storageDisk->path($tmpName), $remotePath);
}
} catch (Throwable $e) { } catch (Throwable $e) {
throw new SSHCommandError( throw new SSHCommandError(
message: $e->getMessage() message: $e->getMessage()

View File

@ -2,8 +2,7 @@
namespace App\Http\Controllers\API; namespace App\Http\Controllers\API;
use App\Actions\FirewallRule\CreateRule; use App\Actions\FirewallRule\ManageRule;
use App\Actions\FirewallRule\DeleteRule;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Resources\FirewallRuleResource; use App\Http\Resources\FirewallRuleResource;
use App\Models\FirewallRule; use App\Models\FirewallRule;
@ -21,6 +20,7 @@
use Spatie\RouteAttributes\Attributes\Middleware; use Spatie\RouteAttributes\Attributes\Middleware;
use Spatie\RouteAttributes\Attributes\Post; use Spatie\RouteAttributes\Attributes\Post;
use Spatie\RouteAttributes\Attributes\Prefix; use Spatie\RouteAttributes\Attributes\Prefix;
use Spatie\RouteAttributes\Attributes\Put;
#[Prefix('api/projects/{project}/servers/{server}/firewall-rules')] #[Prefix('api/projects/{project}/servers/{server}/firewall-rules')]
#[Middleware(['auth:sanctum', 'can-see-project'])] #[Middleware(['auth:sanctum', 'can-see-project'])]
@ -41,10 +41,11 @@ public function index(Project $project, Server $server): ResourceCollection
#[Post('/', name: 'api.projects.servers.firewall-rules.create', middleware: 'ability:write')] #[Post('/', name: 'api.projects.servers.firewall-rules.create', middleware: 'ability:write')]
#[Endpoint(title: 'create', description: 'Create a new firewall rule.')] #[Endpoint(title: 'create', description: 'Create a new firewall rule.')]
#[BodyParam(name: 'name', required: true)]
#[BodyParam(name: 'type', required: true, enum: ['allow', 'deny'])] #[BodyParam(name: 'type', required: true, enum: ['allow', 'deny'])]
#[BodyParam(name: 'protocol', required: true, enum: ['tcp', 'udp'])] #[BodyParam(name: 'protocol', required: true, enum: ['tcp', 'udp'])]
#[BodyParam(name: 'port', required: true)] #[BodyParam(name: 'port', required: true)]
#[BodyParam(name: 'source', required: true)] #[BodyParam(name: 'source', required: false)]
#[BodyParam(name: 'mask', description: 'Mask for source IP.', example: '0')] #[BodyParam(name: 'mask', description: 'Mask for source IP.', example: '0')]
#[ResponseFromApiResource(FirewallRuleResource::class, FirewallRule::class)] #[ResponseFromApiResource(FirewallRuleResource::class, FirewallRule::class)]
public function create(Request $request, Project $project, Server $server): FirewallRuleResource public function create(Request $request, Project $project, Server $server): FirewallRuleResource
@ -53,9 +54,31 @@ public function create(Request $request, Project $project, Server $server): Fire
$this->validateRoute($project, $server); $this->validateRoute($project, $server);
$this->validate($request, CreateRule::rules()); $this->validate($request, ManageRule::rules());
$firewallRule = app(CreateRule::class)->create($server, $request->all()); $firewallRule = app(ManageRule::class)->create($server, $request->all());
return new FirewallRuleResource($firewallRule);
}
#[Put('{firewallRule}', name: 'api.projects.servers.firewall-rules.edit', middleware: 'ability:write')]
#[Endpoint(title: 'edit', description: 'Update an existing firewall rule.')]
#[BodyParam(name: 'name', required: true)]
#[BodyParam(name: 'type', required: true, enum: ['allow', 'deny'])]
#[BodyParam(name: 'protocol', required: true, enum: ['tcp', 'udp'])]
#[BodyParam(name: 'port', required: true)]
#[BodyParam(name: 'source', required: false)]
#[BodyParam(name: 'mask', description: 'Mask for source IP.', example: '0')]
#[ResponseFromApiResource(FirewallRuleResource::class, FirewallRule::class)]
public function edit(Request $request, Project $project, Server $server, FirewallRule $firewallRule): FirewallRuleResource
{
$this->authorize('update', [FirewallRule::class, $firewallRule]);
$this->validateRoute($project, $server);
$this->validate($request, ManageRule::rules());
$firewallRule = app(ManageRule::class)->update($firewallRule, $request->all());
return new FirewallRuleResource($firewallRule); return new FirewallRuleResource($firewallRule);
} }
@ -81,7 +104,7 @@ public function delete(Project $project, Server $server, FirewallRule $firewallR
$this->validateRoute($project, $server, $firewallRule); $this->validateRoute($project, $server, $firewallRule);
app(DeleteRule::class)->delete($server, $firewallRule); app(ManageRule::class)->delete($firewallRule);
return response()->noContent(); return response()->noContent();
} }

View File

@ -29,7 +29,8 @@ public function __invoke(Request $request)
->firstOrFail(); ->firstOrFail();
foreach ($gitHook->actions as $action) { foreach ($gitHook->actions as $action) {
if ($action == 'deploy') { $webhookBranch = $gitHook->site->sourceControl->provider()->getWebhookBranch($request->array());
if ($action == 'deploy' && $gitHook->site->branch === $webhookBranch) {
try { try {
app(Deploy::class)->run($gitHook->site); app(Deploy::class)->run($gitHook->site);
} catch (SourceControlIsNotConnected) { } catch (SourceControlIsNotConnected) {

View File

@ -37,7 +37,7 @@ public function run(Server $server, Request $request)
return response()->stream( return response()->stream(
function () use ($server, $request, $ssh, $log, $currentDir) { function () use ($server, $request, $ssh, $log, $currentDir) {
$command = 'cd '.$currentDir.' && '.$request->command.' && echo "VITO_WORKING_DIR: $(pwd)"'; $command = 'cd '.$currentDir.' && '.$request->command.' && echo -n "VITO_WORKING_DIR: " && pwd';
$output = ''; $output = '';
$ssh->exec(command: $command, log: $log, stream: true, streamCallback: function ($out) use (&$output) { $ssh->exec(command: $command, log: $log, stream: true, streamCallback: function ($out) use (&$output) {
echo preg_replace('/^VITO_WORKING_DIR:.*(\r?\n)?/m', '', $out); echo preg_replace('/^VITO_WORKING_DIR:.*(\r?\n)?/m', '', $out);
@ -63,7 +63,7 @@ function () use ($server, $request, $ssh, $log, $currentDir) {
public function workingDir(Server $server) public function workingDir(Server $server)
{ {
return response()->json([ return response()->json([
'dir' => Cache::get('console.'.$server->id.'.dir'), 'dir' => Cache::get('console.'.$server->id.'.dir', '~'),
]); ]);
} }
} }

View File

@ -13,6 +13,7 @@ public function toArray(Request $request): array
{ {
return [ return [
'id' => $this->id, 'id' => $this->id,
'name' => $this->name,
'server_id' => $this->server_id, 'server_id' => $this->server_id,
'type' => $this->type, 'type' => $this->type,
'protocol' => $this->protocol, 'protocol' => $this->protocol,

71
app/Models/Command.php Normal file
View File

@ -0,0 +1,71 @@
<?php
namespace App\Models;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Support\Collection;
/**
* @property int $id
* @property int $site_id
* @property string $name
* @property string $command
* @property Carbon $created_at
* @property Carbon $updated_at
* @property Collection<CommandExecution> $executions
* @property ?CommandExecution $lastExecution
* @property Site $site
*/
class Command extends AbstractModel
{
use HasFactory;
protected $fillable = [
'site_id',
'name',
'command',
];
protected $casts = [
'site_id' => 'integer',
];
public static function boot(): void
{
parent::boot();
static::deleting(function (Command $command) {
$command->executions()->delete();
});
}
public function site(): BelongsTo
{
return $this->belongsTo(Site::class);
}
public function getVariables(): array
{
$variables = [];
preg_match_all('/\${(.*?)}/', $this->command, $matches);
foreach ($matches[1] as $match) {
$variables[] = $match;
}
return array_unique($variables);
}
public function executions(): HasMany
{
return $this->hasMany(CommandExecution::class);
}
public function lastExecution(): HasOne
{
return $this->hasOne(CommandExecution::class)->latest();
}
}

View File

@ -0,0 +1,83 @@
<?php
namespace App\Models;
use App\Enums\CommandExecutionStatus;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* @property int $id
* @property int $command_id
* @property int $server_id
* @property int $user_id
* @property ?int $server_log_id
* @property array $variables
* @property string $status
* @property Carbon $created_at
* @property Carbon $updated_at
* @property Command $command
* @property ?ServerLog $serverLog
* @property Server $server
* @property ?User $user
*/
class CommandExecution extends AbstractModel
{
use HasFactory;
protected $fillable = [
'command_id',
'server_id',
'user_id',
'server_log_id',
'variables',
'status',
];
protected $casts = [
'command_id' => 'integer',
'server_id' => 'integer',
'user_id' => 'integer',
'server_log_id' => 'integer',
'variables' => 'array',
];
public static array $statusColors = [
CommandExecutionStatus::EXECUTING => 'warning',
CommandExecutionStatus::COMPLETED => 'success',
CommandExecutionStatus::FAILED => 'danger',
];
public function command(): BelongsTo
{
return $this->belongsTo(Command::class);
}
public function getContent(): string
{
$content = $this->command->command;
foreach ($this->variables as $variable => $value) {
if (is_string($value) && ! empty($value)) {
$content = str_replace('${'.$variable.'}', $value, $content);
}
}
return $content;
}
public function serverLog(): BelongsTo
{
return $this->belongsTo(ServerLog::class);
}
public function server(): BelongsTo
{
return $this->belongsTo(Server::class);
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}

147
app/Models/File.php Normal file
View File

@ -0,0 +1,147 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* @property int $user_id
* @property int $server_id
* @property string $path
* @property string $type
* @property string $server_user
* @property string $name
* @property int $size
* @property int $links
* @property string $owner
* @property string $group
* @property string $date
* @property string $permissions
* @property User $user
* @property Server $server
*/
class File extends AbstractModel
{
use HasFactory;
protected $fillable = [
'user_id',
'server_id',
'server_user',
'path',
'type',
'name',
'size',
'links',
'owner',
'group',
'date',
'permissions',
];
protected $casts = [
'user_id' => 'integer',
'server_id' => 'integer',
'size' => 'integer',
'links' => 'integer',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
protected static function boot(): void
{
parent::boot();
static::deleting(function (File $file) {
if ($file->name === '.' || $file->name === '..') {
return false;
}
$file->server->os()->deleteFile($file->getFilePath(), $file->server_user);
return true;
});
}
public function server(): BelongsTo
{
return $this->belongsTo(Server::class);
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public static function path(User $user, Server $server, string $serverUser): string
{
$file = self::query()
->where('user_id', $user->id)
->where('server_id', $server->id)
->where('server_user', $serverUser)
->first();
if ($file) {
return $file->path;
}
return home_path($serverUser);
}
public static function parse(User $user, Server $server, string $path, string $serverUser, string $listOutput): void
{
self::query()
->where('user_id', $user->id)
->where('server_id', $server->id)
->delete();
// Split output by line
$lines = explode("\n", trim($listOutput));
// Skip the first two lines (total count and . & .. directories)
array_shift($lines);
foreach ($lines as $line) {
if (preg_match('/^([drwx\-]+)\s+(\d+)\s+([\w\-]+)\s+([\w\-]+)\s+(\d+)\s+([\w\s:\-]+)\s+(.+)$/', $line, $matches)) {
$type = match ($matches[1][0]) {
'-' => 'file',
'd' => 'directory',
default => 'unknown',
};
if ($type === 'unknown') {
continue;
}
if ($matches[7] === '.') {
continue;
}
self::create([
'user_id' => $user->id,
'server_id' => $server->id,
'server_user' => $serverUser,
'path' => $path,
'type' => $type,
'name' => $matches[7],
'size' => (int) $matches[5],
'links' => (int) $matches[2],
'owner' => $matches[3],
'group' => $matches[4],
'date' => $matches[6],
'permissions' => $matches[1],
]);
}
}
}
public function getFilePath(): string
{
return $this->path.'/'.$this->name;
}
public function isExtractable(): bool
{
$extension = pathinfo($this->name, PATHINFO_EXTENSION);
return in_array($extension, ['zip', 'tar', 'tar.gz', 'bz2', 'tar.bz2']);
}
}

View File

@ -2,11 +2,13 @@
namespace App\Models; namespace App\Models;
use App\Enums\FirewallRuleStatus;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
/** /**
* @property int $server_id * @property int $server_id
* @property string $name
* @property string $type * @property string $type
* @property string $protocol * @property string $protocol
* @property int $port * @property int $port
@ -21,6 +23,7 @@ class FirewallRule extends AbstractModel
use HasFactory; use HasFactory;
protected $fillable = [ protected $fillable = [
'name',
'server_id', 'server_id',
'type', 'type',
'protocol', 'protocol',
@ -36,13 +39,19 @@ class FirewallRule extends AbstractModel
'port' => 'integer', 'port' => 'integer',
]; ];
public function getStatusColor(): string
{
return match ($this->status) {
FirewallRuleStatus::CREATING,
FirewallRuleStatus::UPDATING,
FirewallRuleStatus::DELETING => 'warning',
FirewallRuleStatus::READY => 'success',
FirewallRuleStatus::FAILED => 'danger',
};
}
public function server(): BelongsTo public function server(): BelongsTo
{ {
return $this->belongsTo(Server::class); return $this->belongsTo(Server::class);
} }
public function getRealProtocol(): string
{
return $this->protocol === 'udp' ? 'udp' : 'tcp';
}
} }

View File

@ -5,6 +5,7 @@
use App\Actions\Server\CheckConnection; use App\Actions\Server\CheckConnection;
use App\Enums\ServerStatus; use App\Enums\ServerStatus;
use App\Enums\ServiceStatus; use App\Enums\ServiceStatus;
use App\Exceptions\SSHError;
use App\Facades\SSH; use App\Facades\SSH;
use App\ServerTypes\ServerType; use App\ServerTypes\ServerType;
use App\SSH\Cron\Cron; use App\SSH\Cron\Cron;
@ -22,6 +23,7 @@
use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Throwable;
/** /**
* @property int $project_id * @property int $project_id
@ -146,7 +148,7 @@ public static function boot(): void
} }
$server->provider()->delete(); $server->provider()->delete();
DB::commit(); DB::commit();
} catch (\Throwable $e) { } catch (Throwable $e) {
DB::rollBack(); DB::rollBack();
throw $e; throw $e;
} }
@ -465,6 +467,9 @@ public function cron(): Cron
return new Cron($this); return new Cron($this);
} }
/**
* @throws SSHError
*/
public function checkForUpdates(): void public function checkForUpdates(): void
{ {
$this->updates = $this->os()->availableUpdates(); $this->updates = $this->os()->availableUpdates();
@ -480,4 +485,15 @@ public function getAvailableUpdatesAttribute(?int $value): int
return $value; return $value;
} }
/**
* @throws Throwable
*/
public function download(string $path, string $disk = 'tmp'): void
{
$this->ssh()->download(
Storage::disk($disk)->path(basename($path)),
$path
);
}
} }

View File

@ -41,6 +41,7 @@
* @property Server $server * @property Server $server
* @property ServerLog[] $logs * @property ServerLog[] $logs
* @property Deployment[] $deployments * @property Deployment[] $deployments
* @property Command[] $commands
* @property ?GitHook $gitHook * @property ?GitHook $gitHook
* @property DeploymentScript $deploymentScript * @property DeploymentScript $deploymentScript
* @property Queue[] $queues * @property Queue[] $queues
@ -144,6 +145,11 @@ public function deployments(): HasMany
return $this->hasMany(Deployment::class); return $this->hasMany(Deployment::class);
} }
public function commands(): HasMany
{
return $this->hasMany(Command::class);
}
public function gitHook(): HasOne public function gitHook(): HasOne
{ {
return $this->hasOne(GitHook::class); return $this->hasOne(GitHook::class);

View File

@ -0,0 +1,61 @@
<?php
namespace App\Policies;
use App\Enums\SiteFeature;
use App\Models\Command;
use App\Models\Server;
use App\Models\Site;
use App\Models\User;
use Illuminate\Auth\Access\HandlesAuthorization;
class CommandPolicy
{
use HandlesAuthorization;
public function viewAny(User $user, Site $site, Server $server): bool
{
return ($user->isAdmin() || $server->project->users->contains($user)) &&
$server->isReady() &&
$site->hasFeature(SiteFeature::COMMANDS) &&
$site->isReady();
}
public function view(User $user, Command $command, Site $site, Server $server): bool
{
return ($user->isAdmin() || $server->project->users->contains($user)) &&
$site->server_id === $server->id &&
$server->isReady() &&
$site->isReady() &&
$site->hasFeature(SiteFeature::COMMANDS) &&
$command->site_id === $site->id;
}
public function create(User $user, Site $site, Server $server): bool
{
return ($user->isAdmin() || $server->project->users->contains($user)) &&
$server->isReady() &&
$site->hasFeature(SiteFeature::COMMANDS) &&
$site->isReady();
}
public function update(User $user, Command $command, Site $site, Server $server): bool
{
return ($user->isAdmin() || $server->project->users->contains($user)) &&
$site->server_id === $server->id &&
$server->isReady() &&
$site->isReady() &&
$site->hasFeature(SiteFeature::COMMANDS) &&
$command->site_id === $site->id;
}
public function delete(User $user, Command $command, Site $site, Server $server): bool
{
return ($user->isAdmin() || $server->project->users->contains($user)) &&
$site->server_id === $server->id &&
$server->isReady() &&
$site->isReady() &&
$site->hasFeature(SiteFeature::COMMANDS) &&
$command->site_id === $site->id;
}
}

View File

@ -18,6 +18,9 @@ class AppServiceProvider extends ServiceProvider
*/ */
public function register(): void public function register(): void
{ {
if ($this->app->runningInConsole()) {
return;
}
Fortify::ignoreRoutes(); Fortify::ignoreRoutes();
} }
@ -36,6 +39,8 @@ public function boot(): void
return new FTP; return new FTP;
}); });
Sanctum::usePersonalAccessTokenModel(PersonalAccessToken::class); if (! $this->app->runningInConsole()) {
Sanctum::usePersonalAccessTokenModel(PersonalAccessToken::class);
}
} }
} }

View File

@ -23,6 +23,9 @@ class RouteServiceProvider extends ServiceProvider
*/ */
public function boot(): void public function boot(): void
{ {
if ($this->app->runningInConsole()) {
return;
}
$this->configureRateLimiting(); $this->configureRateLimiting();
} }

View File

@ -15,6 +15,7 @@
use Filament\Panel; use Filament\Panel;
use Filament\Support\Assets\Js; use Filament\Support\Assets\Js;
use Filament\Support\Colors\Color; use Filament\Support\Colors\Color;
use Filament\Support\Enums\MaxWidth;
use Filament\Support\Facades\FilamentAsset; use Filament\Support\Facades\FilamentAsset;
use Filament\Support\Facades\FilamentColor; use Filament\Support\Facades\FilamentColor;
use Filament\Support\Facades\FilamentView; use Filament\Support\Facades\FilamentView;
@ -37,11 +38,18 @@ class WebServiceProvider extends ServiceProvider
*/ */
public function register(): void public function register(): void
{ {
if ($this->app->runningInConsole()) {
return;
}
Filament::registerPanel($this->panel(Panel::make())); Filament::registerPanel($this->panel(Panel::make()));
} }
public function boot(): void public function boot(): void
{ {
if ($this->app->runningInConsole()) {
return;
}
FilamentView::registerRenderHook( FilamentView::registerRenderHook(
PanelsRenderHook::SIDEBAR_NAV_START, PanelsRenderHook::SIDEBAR_NAV_START,
fn () => Livewire::mount(SelectProject::class) fn () => Livewire::mount(SelectProject::class)
@ -89,6 +97,7 @@ public function panel(Panel $panel): Panel
->colors([ ->colors([
'primary' => Color::Indigo, 'primary' => Color::Indigo,
]) ])
->maxContentWidth(MaxWidth::ScreenTwoExtraLarge)
->viteTheme('resources/css/filament/app/theme.css') ->viteTheme('resources/css/filament/app/theme.css')
->brandLogo(fn () => view('components.brand')) ->brandLogo(fn () => view('components.brand'))
->brandLogoHeight('30px') ->brandLogoHeight('30px')

View File

@ -39,4 +39,18 @@ public function checkout(Site $site): void
$site->id $site->id
); );
} }
/**
* @throws SSHError
*/
public function fetchOrigin(Site $site): void
{
$site->server->ssh($site->user)->exec(
view('ssh.git.fetch-origin', [
'path' => $site->path,
]),
'fetch-origin',
$site->id
);
}
} }

View File

@ -3,14 +3,9 @@
namespace App\SSH\OS; namespace App\SSH\OS;
use App\Exceptions\SSHError; use App\Exceptions\SSHError;
use App\Exceptions\SSHUploadFailed;
use App\Models\Server; use App\Models\Server;
use App\Models\ServerLog; use App\Models\ServerLog;
use App\Models\Site; use App\Models\Site;
use Illuminate\Filesystem\FilesystemAdapter;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Throwable;
class OS class OS
{ {
@ -178,24 +173,23 @@ public function reboot(): void
} }
/** /**
* @throws SSHUploadFailed * @deprecated use write() instead
*
* @throws SSHError
*/ */
public function editFile(string $path, ?string $content = null): void public function editFileAs(string $path, string $user, ?string $content = null): void
{ {
$tmpName = Str::random(10).strtotime('now'); $sudo = $user === 'root';
try { $actualUser = $sudo ? $this->server->getSshUser() : $user;
/** @var FilesystemAdapter $storageDisk */
$storageDisk = Storage::disk('local'); $this->server->ssh($actualUser)->exec(
$storageDisk->put($tmpName, $content); view('ssh.os.edit-file', [
$this->server->ssh()->upload( 'path' => $path,
$storageDisk->path($tmpName), 'content' => $content,
$path 'sudo' => $sudo,
); ]),
} catch (Throwable) { 'edit-file'
throw new SSHUploadFailed; );
} finally {
$this->deleteTempFile($tmpName);
}
} }
/** /**
@ -203,11 +197,11 @@ public function editFile(string $path, ?string $content = null): void
*/ */
public function readFile(string $path): string public function readFile(string $path): string
{ {
return $this->server->ssh()->exec( return trim($this->server->ssh()->exec(
view('ssh.os.read-file', [ view('ssh.os.read-file', [
'path' => $path, 'path' => $path,
]) ])
); ));
} }
/** /**
@ -263,10 +257,14 @@ public function download(string $url, string $path): string
/** /**
* @throws SSHError * @throws SSHError
*/ */
public function unzip(string $path): string public function extract(string $path, ?string $destination = null, ?string $user = null): void
{ {
return $this->server->ssh()->exec( $this->server->ssh($user)->exec(
'unzip '.$path view('ssh.os.extract', [
'path' => $path,
'destination' => $destination,
]),
'extract'
); );
} }
@ -304,9 +302,9 @@ public function resourceInfo(): array
/** /**
* @throws SSHError * @throws SSHError
*/ */
public function deleteFile(string $path): void public function deleteFile(string $path, ?string $user = null): void
{ {
$this->server->ssh()->exec( $this->server->ssh($user)->exec(
view('ssh.os.delete-file', [ view('ssh.os.delete-file', [
'path' => $path, 'path' => $path,
]), ]),
@ -314,10 +312,31 @@ public function deleteFile(string $path): void
); );
} }
private function deleteTempFile(string $name): void /**
* @throws SSHError
*/
public function ls(string $path, ?string $user = null): string
{ {
if (Storage::disk('local')->exists($name)) { return $this->server->ssh($user)->exec('ls -la '.$path);
Storage::disk('local')->delete($name); }
}
/**
* @throws SSHError
*/
public function write(string $path, string $content, ?string $user = null): void
{
$this->server->ssh()->write(
$path,
$content,
$user
);
}
/**
* @throws SSHError
*/
public function mkdir(string $path, ?string $user = null): string
{
return $this->server->ssh($user)->exec('mkdir -p '.$path);
} }
} }

View File

@ -4,7 +4,5 @@
interface Firewall interface Firewall
{ {
public function addRule(string $type, string $protocol, int $port, string $source, ?string $mask): void; public function applyRules(): void;
public function removeRule(string $type, string $protocol, int $port, string $source, ?string $mask): void;
} }

View File

@ -2,6 +2,7 @@
namespace App\SSH\Services\Firewall; namespace App\SSH\Services\Firewall;
use App\Enums\FirewallRuleStatus;
use App\Exceptions\SSHError; use App\Exceptions\SSHError;
class Ufw extends AbstractFirewall class Ufw extends AbstractFirewall
@ -26,34 +27,16 @@ public function uninstall(): void
/** /**
* @throws SSHError * @throws SSHError
*/ */
public function addRule(string $type, string $protocol, int $port, string $source, ?string $mask): void public function applyRules(): void
{ {
$this->service->server->ssh()->exec( $rules = $this->service->server
view('ssh.services.firewall.ufw.add-rule', [ ->firewallRules()
'type' => $type, ->where('status', '!=', FirewallRuleStatus::DELETING)
'protocol' => $protocol, ->get();
'port' => $port,
'source' => $source,
'mask' => $mask || $mask === 0 ? '/'.$mask : '',
]),
'add-firewall-rule'
);
}
/**
* @throws SSHError
*/
public function removeRule(string $type, string $protocol, int $port, string $source, ?string $mask): void
{
$this->service->server->ssh()->exec( $this->service->server->ssh()->exec(
view('ssh.services.firewall.ufw.remove-rule', [ view('ssh.services.firewall.ufw.apply-rules', compact('rules')),
'type' => $type, 'apply-rules'
'protocol' => $protocol,
'port' => $port,
'source' => $source,
'mask' => $mask || $mask === 0 ? '/'.$mask : '',
]),
'remove-firewall-rule'
); );
} }
} }

View File

@ -135,7 +135,7 @@ public function createFpmPool(string $user, string $version, $site_id): void
'user' => $user, 'user' => $user,
'version' => $version, 'version' => $version,
]), ]),
true 'root'
); );
$this->service->server->systemd()->restart($this->service->unit); $this->service->server->systemd()->restart($this->service->unit);

View File

@ -55,7 +55,7 @@ public function create(
'numprocs' => (string) $numprocs, 'numprocs' => (string) $numprocs,
'logFile' => $logFile, 'logFile' => $logFile,
]), ]),
true 'root'
); );
$this->service->server->ssh()->exec( $this->service->server->ssh()->exec(

View File

@ -26,7 +26,7 @@ public function install(): void
view('ssh.services.webserver.nginx.nginx', [ view('ssh.services.webserver.nginx.nginx', [
'user' => $this->service->server->getSshUser(), 'user' => $this->service->server->getSshUser(),
]), ]),
true 'root'
); );
$this->service->server->systemd()->restart('nginx'); $this->service->server->systemd()->restart('nginx');
@ -83,7 +83,7 @@ public function createVHost(Site $site): void
view('ssh.services.webserver.nginx.vhost', [ view('ssh.services.webserver.nginx.vhost', [
'site' => $site, 'site' => $site,
]), ]),
true 'root'
); );
$this->service->server->ssh()->exec( $this->service->server->ssh()->exec(
@ -108,7 +108,7 @@ public function updateVHost(Site $site, ?string $vhost = null): void
$vhost ?? view('ssh.services.webserver.nginx.vhost', [ $vhost ?? view('ssh.services.webserver.nginx.vhost', [
'site' => $site, 'site' => $site,
]), ]),
true 'root'
); );
$this->service->server->systemd()->restart('nginx'); $this->service->server->systemd()->restart('nginx');
@ -199,11 +199,13 @@ public function setupSSL(Ssl $ssl): void
*/ */
public function removeSSL(Ssl $ssl): void public function removeSSL(Ssl $ssl): void
{ {
$this->service->server->ssh()->exec( if ($ssl->certificate_path) {
'sudo rm -rf '.dirname($ssl->certificate_path).'*', $this->service->server->ssh()->exec(
'remove-ssl', 'sudo rm -rf '.dirname($ssl->certificate_path),
$ssl->site_id 'remove-ssl',
); $ssl->site_id
);
}
$this->updateVHost($ssl->site); $this->updateVHost($ssl->site);
} }

View File

@ -149,6 +149,10 @@ public function isRunning(): bool
$this->server->save(); $this->server->save();
} }
if (! $this->server->ip) {
return false;
}
if (isset($result['Reservations'][0]['Instances'][0]['State']) && isset($result['Reservations'][0]['Instances'][0]['State']['Name'])) { if (isset($result['Reservations'][0]['Instances'][0]['State']) && isset($result['Reservations'][0]['Instances'][0]['State']['Name'])) {
$status = $result['Reservations'][0]['Instances'][0]['State']['Name']; $status = $result['Reservations'][0]['Instances'][0]['State']['Name'];
if ($status == 'running') { if ($status == 'running') {

View File

@ -176,6 +176,10 @@ public function isRunning(): bool
$this->server->save(); $this->server->save();
} }
if (! $this->server->ip) {
return false;
}
return $status->json()['droplet']['status'] == 'active'; return $status->json()['droplet']['status'] == 'active';
} }

View File

@ -171,6 +171,10 @@ public function isRunning(): bool
$this->server->save(); $this->server->save();
} }
if (! $this->server->ip) {
return false;
}
return $status->json()['instance']['status'] == 'active'; return $status->json()['instance']['status'] == 'active';
} }

View File

@ -37,6 +37,11 @@ public function editRules(array $input): array
return $this->createRules($input); return $this->createRules($input);
} }
public function baseCommands(): array
{
return [];
}
protected function progress(int $percentage): void protected function progress(int $percentage): void
{ {
$this->site->progress = $percentage; $this->site->progress = $percentage;

54
app/SiteTypes/Laravel.php Executable file → Normal file
View File

@ -2,4 +2,56 @@
namespace App\SiteTypes; namespace App\SiteTypes;
class Laravel extends PHPSite {} class Laravel extends PHPSite
{
public function baseCommands(): array
{
return array_merge(parent::baseCommands(), [
// Initial Setup Commands
[
'name' => 'Generate Application Key',
'command' => 'php artisan key:generate',
],
[
'name' => 'Create Storage Symbolic Link',
'command' => 'php artisan storage:link',
],
// Database Commands
[
'name' => 'Run Database Migrations',
'command' => 'php artisan migrate --force',
],
// Cache & Optimization Commands
[
'name' => 'Optimize Application',
'command' => 'php artisan optimize',
],
[
'name' => 'Clear All Application Optimizations',
'command' => 'php artisan optimize:clear',
],
[
'name' => 'Clear Application Cache Only',
'command' => 'php artisan cache:clear',
],
// Queue Commands
[
'name' => 'Restart Queue Workers',
'command' => 'php artisan queue:restart',
],
[
'name' => 'Clear All Failed Queue Jobs',
'command' => 'php artisan queue:flush',
],
// Application State Commands
[
'name' => 'Put Application in Maintenance Mode',
'command' => 'php artisan down --retry=5 --refresh=6 --quiet',
],
[
'name' => 'Bring Application Online',
'command' => 'php artisan up',
],
]);
}
}

View File

@ -13,6 +13,7 @@ public function supportedFeatures(): array
{ {
return [ return [
SiteFeature::DEPLOYMENT, SiteFeature::DEPLOYMENT,
SiteFeature::COMMANDS,
SiteFeature::ENV, SiteFeature::ENV,
SiteFeature::SSL, SiteFeature::SSL,
SiteFeature::QUEUES, SiteFeature::QUEUES,
@ -55,4 +56,9 @@ public function install(): void
$this->progress(65); $this->progress(65);
$this->site->php()?->restart(); $this->site->php()?->restart();
} }
public function baseCommands(): array
{
return [];
}
} }

View File

@ -21,12 +21,23 @@ public function supportedFeatures(): array
{ {
return [ return [
SiteFeature::DEPLOYMENT, SiteFeature::DEPLOYMENT,
SiteFeature::COMMANDS,
SiteFeature::ENV, SiteFeature::ENV,
SiteFeature::SSL, SiteFeature::SSL,
SiteFeature::QUEUES, SiteFeature::QUEUES,
]; ];
} }
public function baseCommands(): array
{
return [
[
'name' => 'Install Composer Dependencies',
'command' => 'composer install --no-dev --no-interaction --no-progress',
],
];
}
public function createRules(array $input): array public function createRules(array $input): array
{ {
return [ return [

View File

@ -19,4 +19,6 @@ public function install(): void;
public function editRules(array $input): array; public function editRules(array $input): array;
public function edit(): void; public function edit(): void;
public function baseCommands(): array;
} }

View File

@ -24,6 +24,7 @@ public function supportedFeatures(): array
{ {
return [ return [
SiteFeature::SSL, SiteFeature::SSL,
SiteFeature::COMMANDS,
]; ];
} }

View File

@ -70,4 +70,9 @@ protected function handleResponseErrors(Response $res, string $repo): void
throw new RepositoryPermissionDenied($repo); throw new RepositoryPermissionDenied($repo);
} }
} }
public function getWebhookBranch(array $payload): string
{
return str($payload['ref'] ?? '')->after('refs/heads/')->toString();
}
} }

View File

@ -183,4 +183,9 @@ private function getAuthenticationHeaders(): array
'Authorization' => 'Basic '.$basicAuth, 'Authorization' => 'Basic '.$basicAuth,
]; ];
} }
public function getWebhookBranch(array $payload): string
{
return data_get($payload, 'push.changes.0.new.name', 'default-branch');
}
} }

View File

@ -176,4 +176,9 @@ public function getApiUrl(): string
return $host.$this->apiVersion; return $host.$this->apiVersion;
} }
public function getWebhookBranch(array $payload): string
{
return $payload['ref'] ?? '';
}
} }

View File

@ -36,4 +36,6 @@ public function getLastCommit(string $repo, string $branch): ?array;
* @throws FailedToDeployGitKey * @throws FailedToDeployGitKey
*/ */
public function deployKey(string $title, string $repo, string $key): void; public function deployKey(string $title, string $repo, string $key): void;
public function getWebhookBranch(array $payload): string;
} }

View File

@ -82,7 +82,7 @@ public function exec(string $command, string $log = '', ?int $siteId = null, ?bo
return $output; return $output;
} }
public function upload(string $local, string $remote): void public function upload(string $local, string $remote, ?string $owner = null): void
{ {
$this->uploadedLocalPath = $local; $this->uploadedLocalPath = $local;
$this->uploadedRemotePath = $remote; $this->uploadedRemotePath = $remote;

View File

@ -178,3 +178,31 @@ function get_from_route(string $modelName, string $routeKey): mixed
return null; return null;
} }
function absolute_path(string $path): string
{
$parts = explode('/', $path);
$absoluteParts = [];
foreach ($parts as $part) {
if ($part === '' || $part === '.') {
continue; // Skip empty and current directory parts
}
if ($part === '..') {
array_pop($absoluteParts); // Move up one directory
} else {
$absoluteParts[] = $part; // Add valid directory parts
}
}
return '/'.implode('/', $absoluteParts);
}
function home_path(string $user): string
{
if ($user === 'root') {
return '/root';
}
return '/home/'.$user;
}

View File

@ -64,7 +64,7 @@ protected function getHeaderActions(): array
]; ];
foreach ($this->script->getVariables() as $variable) { foreach ($this->script->getVariables() as $variable) {
$form[] = TextInput::make($variable) $form[] = TextInput::make('variables.'.$variable)
->label($variable) ->label($variable)
->rules(fn (Get $get) => ExecuteScript::rules($get())['variables.*']); ->rules(fn (Get $get) => ExecuteScript::rules($get())['variables.*']);
} }

View File

@ -0,0 +1,26 @@
<?php
namespace App\Web\Pages\Servers\FileManager;
use App\Web\Pages\Servers\Page;
class Index extends Page
{
protected static ?string $slug = 'servers/{server}/file-manager';
protected static ?string $title = 'File Manager';
protected $listeners = ['$refresh'];
public function mount(): void
{
$this->authorize('manage', $this->server);
}
public function getWidgets(): array
{
return [
[Widgets\FilesList::class, ['server' => $this->server]],
];
}
}

View File

@ -0,0 +1,372 @@
<?php
namespace App\Web\Pages\Servers\FileManager\Widgets;
use App\Actions\FileManager\FetchFiles;
use App\Exceptions\SSHError;
use App\Models\File;
use App\Models\Server;
use App\Web\Fields\CodeEditorField;
use App\Web\Pages\Servers\FileManager\Index;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\TextInput;
use Filament\Support\Enums\ActionSize;
use Filament\Support\Enums\IconPosition;
use Filament\Tables\Actions\Action;
use Filament\Tables\Actions\ActionGroup;
use Filament\Tables\Actions\DeleteBulkAction;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Filament\Widgets\TableWidget as Widget;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Storage;
class FilesList extends Widget
{
public Server $server;
public string $serverUser;
public string $path;
protected $listeners = ['$refresh'];
public function mount(): void
{
$this->serverUser = $this->server->ssh_user;
$this->path = home_path($this->serverUser);
if (request()->has('path') && request()->has('user')) {
$this->path = request('path');
$this->serverUser = request('user');
}
$this->refresh();
}
protected function getTableHeaderActions(): array
{
return [
$this->homeAction(),
$this->userAction(),
ActionGroup::make([
$this->refreshAction(),
$this->newFileAction(),
$this->newDirectoryAction(),
$this->uploadAction(),
])
->tooltip('Toolbar')
->icon('heroicon-o-ellipsis-vertical')
->color('gray')
->size(ActionSize::Large)
->iconPosition(IconPosition::After)
->dropdownPlacement('bottom-end'),
];
}
protected function getTableQuery(): Builder
{
return File::query()
->where('user_id', auth()->id())
->where('server_id', $this->server->id);
}
public function table(Table $table): Table
{
return $table
->query($this->getTableQuery())
->headerActions($this->getTableHeaderActions())
->heading(str($this->path)->substr(-50)->start(str($this->path)->length() > 50 ? '...' : ''))
->columns([
IconColumn::make('type')
->sortable()
->icon(fn (File $file) => $this->getIcon($file)),
TextColumn::make('name')
->sortable(),
TextColumn::make('size')
->sortable(),
TextColumn::make('owner')
->sortable(),
TextColumn::make('group')
->sortable(),
TextColumn::make('date')
->sortable(),
TextColumn::make('permissions')
->sortable(),
])
->recordUrl(function (File $file) {
if ($file->type === 'directory') {
return Index::getUrl([
'server' => $this->server->id,
'user' => $file->server_user,
'path' => absolute_path($file->path.'/'.$file->name),
]);
}
return '';
})
->defaultSort('type')
->actions([
$this->extractAction(),
$this->downloadAction(),
$this->editAction(),
$this->deleteAction(),
])
->checkIfRecordIsSelectableUsing(
fn (File $file): bool => $file->name !== '..',
)
->bulkActions([
DeleteBulkAction::make()
->requiresConfirmation(),
]);
}
public function changeUser(string $user): void
{
$this->redirect(
Index::getUrl([
'server' => $this->server->id,
'user' => $user,
'path' => home_path($user),
]),
true
);
}
public function refresh(): void
{
try {
app(FetchFiles::class)->fetch(
auth()->user(),
$this->server,
[
'user' => $this->serverUser,
'path' => $this->path,
]
);
} catch (SSHError) {
abort(404);
}
$this->dispatch('$refresh');
}
protected function getIcon(File $file): string
{
if ($file->type === 'directory') {
return 'heroicon-o-folder';
}
if (str($file->name)->endsWith('.blade.php')) {
return 'laravel';
}
if (str($file->name)->endsWith('.php')) {
return 'php';
}
return 'heroicon-o-document-text';
}
protected function homeAction(): Action
{
return Action::make('home')
->label('Home')
->size(ActionSize::Small)
->icon('heroicon-o-home')
->action(function () {
$this->path = home_path($this->serverUser);
$this->refresh();
});
}
protected function userAction(): ActionGroup
{
$users = [];
foreach ($this->server->getSshUsers() as $user) {
$users[] = Action::make('user-'.$user)
->action(fn () => $this->changeUser($user))
->label($user);
}
return ActionGroup::make($users)
->tooltip('Change user')
->label($this->serverUser)
->button()
->size(ActionSize::Small)
->color('gray')
->icon('heroicon-o-chevron-up-down')
->iconPosition(IconPosition::After)
->dropdownPlacement('bottom-end');
}
protected function refreshAction(): Action
{
return Action::make('refresh')
->label('Refresh')
->icon('heroicon-o-arrow-path')
->action(fn () => $this->refresh());
}
protected function newFileAction(): Action
{
return Action::make('new-file')
->label('New File')
->icon('heroicon-o-document-text')
->action(function (array $data) {
run_action($this, function () use ($data) {
$this->server->os()->write(
$this->path.'/'.$data['name'],
str_replace("\r\n", "\n", $data['content']),
$this->serverUser
);
$this->refresh();
});
})
->form(function () {
return [
TextInput::make('name')
->placeholder('file-name.txt'),
CodeEditorField::make('content'),
];
})
->modalSubmitActionLabel('Create')
->modalHeading('New File')
->modalWidth('4xl');
}
protected function newDirectoryAction(): Action
{
return Action::make('new-directory')
->label('New Directory')
->icon('heroicon-o-folder')
->action(function (array $data) {
run_action($this, function () use ($data) {
$this->server->os()->mkdir(
$this->path.'/'.$data['name'],
$this->serverUser
);
$this->refresh();
});
})
->form(function () {
return [
TextInput::make('name')
->placeholder('directory name'),
];
})
->modalSubmitActionLabel('Create')
->modalHeading('New Directory')
->modalWidth('lg');
}
protected function uploadAction(): Action
{
return Action::make('upload')
->label('Upload File')
->icon('heroicon-o-arrow-up-on-square')
->action(function (array $data) {
//
})
->after(function (array $data) {
run_action($this, function () use ($data) {
foreach ($data['file'] as $file) {
$this->server->ssh()->upload(
Storage::disk('tmp')->path($file),
$this->path.'/'.$file,
$this->serverUser
);
}
$this->refresh();
});
})
->form(function () {
return [
FileUpload::make('file')
->disk('tmp')
->multiple()
->preserveFilenames(),
];
})
->modalSubmitActionLabel('Upload to Server')
->modalHeading('Upload File')
->modalWidth('xl');
}
protected function extractAction(): Action
{
return Action::make('extract')
->tooltip('Extract')
->icon('heroicon-o-archive-box')
->hiddenLabel()
->visible(fn (File $file) => $file->isExtractable())
->action(function (File $file) {
$file->server->os()->extract($file->getFilePath(), $file->path, $file->server_user);
$this->refresh();
});
}
protected function downloadAction(): Action
{
return Action::make('download')
->tooltip('Download')
->icon('heroicon-o-arrow-down-tray')
->hiddenLabel()
->visible(fn (File $file) => $file->type === 'file')
->action(function (File $file) {
$file->server->ssh($file->server_user)->download(
Storage::disk('tmp')->path($file->name),
$file->getFilePath()
);
return Storage::disk('tmp')->download($file->name);
});
}
protected function editAction(): Action
{
return Action::make('edit')
->tooltip('Edit')
->icon('heroicon-o-pencil')
->hiddenLabel()
->visible(fn (File $file) => $file->type === 'file')
->action(function (File $file, array $data) {
$file->server->os()->write(
$file->getFilePath(),
str_replace("\r\n", "\n", $data['content']),
$file->server_user
);
$this->refresh();
})
->form(function (File $file) {
return [
CodeEditorField::make('content')
->formatStateUsing(function () use ($file) {
$file->server->ssh($file->server_user)->download(
Storage::disk('tmp')->path($file->name),
$file->getFilePath()
);
return Storage::disk('tmp')->get(basename($file->getFilePath()));
}),
];
})
->modalSubmitActionLabel('Save')
->modalHeading('Edit')
->modalWidth('4xl');
}
protected function deleteAction(): Action
{
return Action::make('delete')
->tooltip('Delete')
->icon('heroicon-o-trash')
->color('danger')
->hiddenLabel()
->requiresConfirmation()
->visible(fn (File $file) => $file->name !== '..')
->action(function (File $file) {
run_action($this, function () use ($file) {
$file->delete();
});
});
}
}

View File

@ -2,14 +2,17 @@
namespace App\Web\Pages\Servers\Firewall; namespace App\Web\Pages\Servers\Firewall;
use App\Actions\FirewallRule\CreateRule; use App\Actions\FirewallRule\ManageRule;
use App\Models\FirewallRule; use App\Models\FirewallRule;
use App\Web\Pages\Servers\Page; use App\Web\Pages\Servers\Page;
use Filament\Actions\Action; use Filament\Actions\Action;
use Filament\Forms\Components\Checkbox;
use Filament\Forms\Components\Select; use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Forms\Get;
use Filament\Notifications\Notification; use Filament\Notifications\Notification;
use Filament\Support\Enums\MaxWidth; use Filament\Support\Enums\MaxWidth;
use Illuminate\Support\Facades\Request;
class Index extends Page class Index extends Page
{ {
@ -31,6 +34,64 @@ public function getWidgets(): array
]; ];
} }
public static function getFirewallForm(?FirewallRule $record = null): array
{
return [
TextInput::make('name')
->label('Purpose')
->default($record->name ?? null)
->rules(ManageRule::rules()['name']),
Select::make('type')
->label('Type')
->default($record->type ?? 'allow')
->options([
'allow' => 'Allow',
'deny' => 'Deny',
])
->rules(ManageRule::rules()['type']),
Select::make('protocol')
->label('Protocol')
->default($record->protocol ?? 'tcp')
->options([
'tcp' => 'TCP',
'udp' => 'UDP',
])
->rules(ManageRule::rules()['protocol']),
TextInput::make('port')
->label('Port')
->default($record->port ?? null)
->rules(['required', 'integer']),
Checkbox::make('source_any')
->label('Any Source')
->default(($record->source ?? null) == null)
->rules(['boolean'])
->helperText('Allow connections from any source, regardless of their IP address or subnet mask.')
->live(),
TextInput::make('source')
->hidden(fn (Get $get) => $get('source_any') == true)
->label('Source')
->helperText('The IP address of the source of the connection.')
->rules(ManageRule::rules()['source'])
->default($record->source ?? null)
->suffixAction(
\Filament\Forms\Components\Actions\Action::make('get_ip')
->icon('heroicon-o-globe-alt')
->color('primary')
->tooltip('Use My IP')
->action(function ($set) {
$ip = Request::ip();
$set('source', $ip);
})
),
TextInput::make('mask')
->hidden(fn (Get $get) => $get('source_any') == true)
->label('Mask')
->default($record->mask ?? null)
->helperText('The subnet mask of the source of the connection. Leave blank for a single IP address.')
->rules(ManageRule::rules()['mask']),
];
}
protected function getHeaderActions(): array protected function getHeaderActions(): array
{ {
return [ return [
@ -45,37 +106,19 @@ protected function getHeaderActions(): array
->label('Create a Rule') ->label('Create a Rule')
->icon('heroicon-o-plus') ->icon('heroicon-o-plus')
->modalWidth(MaxWidth::Large) ->modalWidth(MaxWidth::Large)
->form([ ->modalHeading('Create Firewall Rule')
Select::make('type') ->modalDescription('Add a new rule to the firewall')
->native(false) ->modalSubmitActionLabel('Create')
->options([ ->form(self::getFirewallForm())
'allow' => 'Allow',
'deny' => 'Deny',
])
->rules(CreateRule::rules()['type']),
Select::make('protocol')
->native(false)
->options([
'tcp' => 'TCP',
'udp' => 'UDP',
])
->rules(CreateRule::rules()['protocol']),
TextInput::make('port')
->rules(CreateRule::rules()['port']),
TextInput::make('source')
->rules(CreateRule::rules()['source']),
TextInput::make('mask')
->rules(CreateRule::rules()['mask']),
])
->action(function (array $data) { ->action(function (array $data) {
run_action($this, function () use ($data) { run_action($this, function () use ($data) {
app(CreateRule::class)->create($this->server, $data); app(ManageRule::class)->create($this->server, $data);
$this->dispatch('$refresh'); $this->dispatch('$refresh');
Notification::make() Notification::make()
->success() ->success()
->title('Firewall rule created!') ->title('Applying Firewall Rule')
->send(); ->send();
}); });
}), }),

View File

@ -2,15 +2,18 @@
namespace App\Web\Pages\Servers\Firewall\Widgets; namespace App\Web\Pages\Servers\Firewall\Widgets;
use App\Actions\FirewallRule\DeleteRule; use App\Actions\FirewallRule\ManageRule;
use App\Models\FirewallRule; use App\Models\FirewallRule;
use App\Models\Server; use App\Models\Server;
use App\Web\Pages\Servers\Firewall\Index;
use Filament\Notifications\Notification; use Filament\Notifications\Notification;
use Filament\Support\Enums\MaxWidth;
use Filament\Tables\Actions\Action; use Filament\Tables\Actions\Action;
use Filament\Tables\Columns\TextColumn; use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table; use Filament\Tables\Table;
use Filament\Widgets\TableWidget as Widget; use Filament\Widgets\TableWidget as Widget;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Str;
class RulesList extends Widget class RulesList extends Widget
{ {
@ -26,19 +29,40 @@ protected function getTableQuery(): Builder
protected function getTableColumns(): array protected function getTableColumns(): array
{ {
return [ return [
TextColumn::make('name')
->searchable()
->sortable()
->label('Purpose'),
TextColumn::make('type') TextColumn::make('type')
->sortable() ->sortable()
->extraAttributes(['class' => 'uppercase']) ->badge()
->color(fn (FirewallRule $record) => $record->type === 'allow' ? 'green' : 'red'), ->color(fn ($state) => $state === 'allow' ? 'success' : 'warning')
->label('Type')
->formatStateUsing(fn ($state) => Str::upper($state)),
TextColumn::make('id')
->sortable()
->label('Source')
->formatStateUsing(function (FirewallRule $record) {
$source = $record->source == null ? 'any' : $record->source;
if ($source !== 'any' && $record->mask !== null) {
$source .= '/'.$record->mask;
}
return $source;
}),
TextColumn::make('protocol') TextColumn::make('protocol')
->sortable() ->sortable()
->extraAttributes(['class' => 'uppercase']), ->badge()
->color('primary')
->label('Protocol')
->formatStateUsing(fn ($state) => Str::upper($state)),
TextColumn::make('port') TextColumn::make('port')
->sortable(), ->sortable()
TextColumn::make('source') ->label('Port'),
->sortable(), TextColumn::make('status')
TextColumn::make('mask') ->label('Status')
->sortable(), ->badge()
->color(fn (FirewallRule $record) => $record->getStatusColor()),
]; ];
} }
@ -49,6 +73,28 @@ public function table(Table $table): Table
->query($this->getTableQuery()) ->query($this->getTableQuery())
->columns($this->getTableColumns()) ->columns($this->getTableColumns())
->actions([ ->actions([
Action::make('edit')
->icon('heroicon-o-pencil')
->tooltip('Edit')
->hiddenLabel()
->modalWidth(MaxWidth::Large)
->modalHeading('Edit Firewall Rule')
->modalDescription('Edit the associated servers firewall rule.')
->modalSubmitActionLabel('Update')
->authorize(fn (FirewallRule $record) => auth()->user()->can('update', $record))
->form(fn ($record) => Index::getFirewallForm($record))
->action(function (FirewallRule $record, array $data) {
run_action($this, function () use ($record, $data) {
app(ManageRule::class)->update($record, $data);
$this->dispatch('$refresh');
Notification::make()
->success()
->title('Applying Firewall Rule')
->send();
});
}),
Action::make('delete') Action::make('delete')
->icon('heroicon-o-trash') ->icon('heroicon-o-trash')
->tooltip('Delete') ->tooltip('Delete')
@ -58,7 +104,7 @@ public function table(Table $table): Table
->authorize(fn (FirewallRule $record) => auth()->user()->can('delete', $record)) ->authorize(fn (FirewallRule $record) => auth()->user()->can('delete', $record))
->action(function (FirewallRule $record) { ->action(function (FirewallRule $record) {
try { try {
app(DeleteRule::class)->delete($this->server, $record); app(ManageRule::class)->delete($record);
} catch (\Exception $e) { } catch (\Exception $e) {
Notification::make() Notification::make()
->danger() ->danger()

View File

@ -15,6 +15,7 @@
use App\Web\Pages\Servers\Console\Index as ConsoleIndex; use App\Web\Pages\Servers\Console\Index as ConsoleIndex;
use App\Web\Pages\Servers\CronJobs\Index as CronJobsIndex; use App\Web\Pages\Servers\CronJobs\Index as CronJobsIndex;
use App\Web\Pages\Servers\Databases\Index as DatabasesIndex; use App\Web\Pages\Servers\Databases\Index as DatabasesIndex;
use App\Web\Pages\Servers\FileManager\Index as FileManagerIndex;
use App\Web\Pages\Servers\Firewall\Index as FirewallIndex; use App\Web\Pages\Servers\Firewall\Index as FirewallIndex;
use App\Web\Pages\Servers\Logs\Index as LogsIndex; use App\Web\Pages\Servers\Logs\Index as LogsIndex;
use App\Web\Pages\Servers\Metrics\Index as MetricsIndex; use App\Web\Pages\Servers\Metrics\Index as MetricsIndex;
@ -59,6 +60,13 @@ public function getSubNavigation(): array
->url(DatabasesIndex::getUrl(parameters: ['server' => $this->server])); ->url(DatabasesIndex::getUrl(parameters: ['server' => $this->server]));
} }
if (auth()->user()->can('manage', $this->server)) {
$items[] = NavigationItem::make(FileManagerIndex::getNavigationLabel())
->icon('heroicon-o-folder')
->isActiveWhen(fn () => request()->routeIs(FileManagerIndex::getRouteName().'*'))
->url(FileManagerIndex::getUrl(parameters: ['server' => $this->server]));
}
if (auth()->user()->can('viewAny', [Service::class, $this->server])) { if (auth()->user()->can('viewAny', [Service::class, $this->server])) {
$items[] = NavigationItem::make(PHPIndex::getNavigationLabel()) $items[] = NavigationItem::make(PHPIndex::getNavigationLabel())
->icon('icon-php-alt') ->icon('icon-php-alt')

View File

@ -56,6 +56,10 @@ public function getWidgets(): array
} }
if ($this->site->isReady()) { if ($this->site->isReady()) {
if (in_array(SiteFeature::COMMANDS, $this->site->type()->supportedFeatures())) {
$widgets[] = [Widgets\Commands::class, ['site' => $this->site]];
}
if (in_array(SiteFeature::DEPLOYMENT, $this->site->type()->supportedFeatures())) { if (in_array(SiteFeature::DEPLOYMENT, $this->site->type()->supportedFeatures())) {
$widgets[] = [Widgets\DeploymentsList::class, ['site' => $this->site]]; $widgets[] = [Widgets\DeploymentsList::class, ['site' => $this->site]];
} }

View File

@ -0,0 +1,176 @@
<?php
namespace App\Web\Pages\Servers\Sites\Widgets;
use App\Actions\Site\CreateCommand;
use App\Actions\Site\EditCommand;
use App\Actions\Site\ExecuteCommand;
use App\Models\Command;
use App\Models\CommandExecution;
use App\Models\Site;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Get;
use Filament\Notifications\Notification;
use Filament\Support\Enums\MaxWidth;
use Filament\Tables\Actions\Action;
use Filament\Tables\Actions\DeleteAction;
use Filament\Tables\Actions\EditAction;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Filament\Widgets\TableWidget as Widget;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\View\ComponentAttributeBag;
class Commands extends Widget
{
public Site $site;
protected $listeners = ['$refresh'];
protected function getTableQuery(): Builder
{
return Command::query()->where('site_id', $this->site->id);
}
protected function applySortingToTableQuery(Builder $query): Builder
{
return $query->latest('created_at');
}
protected function getTableColumns(): array
{
return [
TextColumn::make('name'),
TextColumn::make('lastExecution.status')
->label('Status')
->badge()
->color(fn (Command $record) => CommandExecution::$statusColors[$record->lastExecution?->status])
->sortable(),
TextColumn::make('created_at')
->label('Last Execution At')
->formatStateUsing(fn (Command $record) => $record->lastExecution?->created_at_by_timezone)
->searchable()
->sortable(),
];
}
protected function getTableHeaderActions(): array
{
return [
Action::make('new-command')
->label('Create a Command')
->modalDescription('The command will be executed inside the site\'s directory')
->icon('heroicon-o-plus')
->authorize(fn () => auth()->user()->can('create', [Command::class, $this->site, $this->site->server]))
->action(function (array $data) {
run_action($this, function () use ($data) {
app(CreateCommand::class)->create($this->site, $data);
$this->dispatch('$refresh');
Notification::make()
->success()
->title('Command created!')
->send();
});
})
->form([
TextInput::make('name')
->rules(CreateCommand::rules()['name']),
TextInput::make('command')
->placeholder('php artisan my:command')
->rules(CreateCommand::rules()['command'])
->helperText('You can use variables like ${VARIABLE_NAME} in the command. The variables will be asked when executing the command'),
])
->modalSubmitActionLabel('Create')
->modalHeading('New Command')
->modalWidth('md'),
];
}
public function table(Table $table): Table
{
return $table
->query($this->getTableQuery())
->headerActions($this->getTableHeaderActions())
->columns($this->getTableColumns())
->heading('Commands')
->defaultPaginationPageOption(5)
->searchable(false)
->actions([
Action::make('execute')
->hiddenLabel()
->tooltip('Execute')
->icon('heroicon-o-play')
->modalWidth(MaxWidth::Medium)
->modalSubmitActionLabel('Execute')
->form(function (Command $record) {
$form = [
TextInput::make('command')->default($record->command)->disabled(),
];
foreach ($record->getVariables() as $variable) {
$form[] = TextInput::make('variables.'.$variable)
->label($variable)
->rules(fn (Get $get) => ExecuteCommand::rules($get())['variables.*']);
}
return $form;
})
->authorize(fn (Command $record) => auth()->user()->can('update', [$record->site, $record->site->server]))
->action(function (array $data, Command $record) {
/** @var \App\Models\User $user */
$user = auth()->user();
app(ExecuteCommand::class)->execute($record, $user, $data);
$this->dispatch('$refresh');
}),
Action::make('logs')
->hiddenLabel()
->tooltip('Last Log')
->icon('heroicon-o-eye')
->modalHeading('View Last Execution Log')
->modalContent(function (Command $record) {
return view('components.console-view', [
'slot' => $record->lastExecution?->serverLog?->getContent() ?? 'Not executed yet',
'attributes' => new ComponentAttributeBag,
]);
})
->modalSubmitAction(false)
->modalCancelActionLabel('Close'),
EditAction::make('edit')
->hiddenLabel()
->tooltip('Edit')
->modalHeading('Edit Command')
->mutateRecordDataUsing(function (array $data, Command $record) {
return [
'name' => $record->name,
'command' => $record->command,
];
})
->form([
TextInput::make('name')
->rules(EditCommand::rules()['name']),
TextInput::make('command')
->rules(EditCommand::rules()['command'])
->helperText('You can use variables like ${VARIABLE_NAME} in the command. The variables will be asked when executing the command'),
])
->authorize(fn (Command $record) => auth()->user()->can('update', [$record, $this->site, $this->site->server]))
->using(function (array $data, Command $record) {
app(EditCommand::class)->edit($record, $data);
$this->dispatch('$refresh');
})
->modalWidth(MaxWidth::Medium),
DeleteAction::make('delete')
->icon('heroicon-o-trash')
->hiddenLabel()
->tooltip('Delete')
->modalHeading('Delete Command')
->authorize(fn (Command $record) => auth()->user()->can('delete', [$record, $this->site, $this->site->server]))
->using(function (array $data, Command $record) {
$record->delete();
}),
]);
}
}

View File

@ -180,6 +180,16 @@ public function infolist(Infolist $infolist): Infolist
}); });
}) })
), ),
TextEntry::make('repository')
->label('Repository')
->visible(fn (Site $record) => $record->repository)
->formatStateUsing(fn (Site $record) => $record->repository)
->inlineLabel(),
TextEntry::make('branch')
->label('Branch')
->visible(fn (Site $record) => $record->branch)
->formatStateUsing(fn (Site $record) => $record->branch)
->inlineLabel(),
]), ]),
]) ])
->record($this->site); ->record($this->site);

2
bootstrap/cli-cache/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*
!.gitignore

69
bootstrap/cli.php Normal file
View File

@ -0,0 +1,69 @@
<?php
use Illuminate\Foundation\Application;
putenv('APP_SERVICES_CACHE='.__DIR__.'/cli-cache/services.php');
putenv('APP_PACKAGES_CACHE='.__DIR__.'/cli-cache/packages.php');
putenv('APP_CONFIG_CACHE='.__DIR__.'/cli-cache/config.php');
putenv('APP_ROUTES_CACHE='.__DIR__.'/cli-cache/routes.php');
putenv('APP_EVENTS_CACHE='.__DIR__.'/cli-cache/events.php');
putenv('LOG_CHANNEL=syslog');
putenv('QUEUE_CONNECTION=sync');
putenv('CACHE_DRIVER=null');
putenv('DB_DATABASE=database.sqlite');
/*
|--------------------------------------------------------------------------
| Create The Application
|--------------------------------------------------------------------------
|
| The first thing we will do is create a new Laravel application instance
| which serves as the "glue" for all the components of Laravel, and is
| the IoC container for the system binding all of the various parts.
|
*/
$app = new Application(
$_ENV['APP_BASE_PATH'] ?? dirname(__DIR__)
);
$app->useStoragePath(getenv('HOME') . '/.vito/storage');
/*
|--------------------------------------------------------------------------
| Bind Important Interfaces
|--------------------------------------------------------------------------
|
| Next, we need to bind some important interfaces into the container so
| we will be able to resolve them when needed. The kernels serve the
| incoming requests to this application from both the web and CLI.
|
*/
// $app->singleton(
// Illuminate\Contracts\Http\Kernel::class,
// App\Http\Kernel::class
// );
$app->singleton(
Illuminate\Contracts\Console\Kernel::class,
App\Cli\Kernel::class
);
$app->singleton(
Illuminate\Contracts\Debug\ExceptionHandler::class,
App\Exceptions\Handler::class
);
/*
|--------------------------------------------------------------------------
| Return The Application
|--------------------------------------------------------------------------
|
| This script returns the application instance. The instance is given to
| the calling script so we can separate the building of the instances
| from the actual running of the application and sending responses.
|
*/
return $app;

20
box.json Normal file
View File

@ -0,0 +1,20 @@
{
"directories": [
"app",
"bootstrap",
"config",
"database",
"public",
"resources",
"routes",
"storage",
"vendor"
],
"files": [
"artisan"
],
"main": "cli",
"output": "vito-cli.phar",
"chmod": "0755",
"stub": true
}

53
cli Normal file
View File

@ -0,0 +1,53 @@
#!/usr/bin/env php
<?php
define('LARAVEL_START', microtime(true));
/*
|--------------------------------------------------------------------------
| Register The Auto Loader
|--------------------------------------------------------------------------
|
| Composer provides a convenient, automatically generated class loader
| for our application. We just need to utilize it! We'll require it
| into the script here so that we do not have to worry about the
| loading of any our classes "manually". Feels great to relax.
|
*/
$autoloader = require file_exists(__DIR__.'/vendor/autoload.php') ? __DIR__.'/vendor/autoload.php' : __DIR__.'/../../autoload.php';
$app = require_once __DIR__.'/bootstrap/cli.php';
/*
|--------------------------------------------------------------------------
| Run The Artisan Application
|--------------------------------------------------------------------------
|
| When we run the console application, the current CLI command will be
| executed in this console and the response sent back to a terminal
| or another output device for the developers. Here goes nothing!
|
*/
$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);
$status = $kernel->handle(
$input = new Symfony\Component\Console\Input\ArgvInput,
new Symfony\Component\Console\Output\ConsoleOutput
);
/*
|--------------------------------------------------------------------------
| Shutdown The Application
|--------------------------------------------------------------------------
|
| Once Artisan has finished running, we will fire off the shutdown events
| so that any final work may be done by the application before we shut
| down the process. This is the last thing to happen to the request.
|
*/
$kernel->terminate($input, $status);
exit($status);

View File

@ -15,6 +15,7 @@
"filament/filament": "^3.2", "filament/filament": "^3.2",
"laravel/fortify": "^1.17", "laravel/fortify": "^1.17",
"laravel/framework": "^11.0", "laravel/framework": "^11.0",
"laravel/prompts": "^0.3.5",
"laravel/sanctum": "^4.0", "laravel/sanctum": "^4.0",
"laravel/tinker": "^2.8", "laravel/tinker": "^2.8",
"mobiledetect/mobiledetectlib": "^4.8", "mobiledetect/mobiledetectlib": "^4.8",

16
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "be3e63b7efd71f649cbffb0d469ba7c1", "content-hash": "e211da7974e07c3b74ad59ce245a9446",
"packages": [ "packages": [
{ {
"name": "anourvalar/eloquent-serialize", "name": "anourvalar/eloquent-serialize",
@ -2559,16 +2559,16 @@
}, },
{ {
"name": "laravel/prompts", "name": "laravel/prompts",
"version": "v0.3.3", "version": "v0.3.5",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/laravel/prompts.git", "url": "https://github.com/laravel/prompts.git",
"reference": "749395fcd5f8f7530fe1f00dfa84eb22c83d94ea" "reference": "57b8f7efe40333cdb925700891c7d7465325d3b1"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/laravel/prompts/zipball/749395fcd5f8f7530fe1f00dfa84eb22c83d94ea", "url": "https://api.github.com/repos/laravel/prompts/zipball/57b8f7efe40333cdb925700891c7d7465325d3b1",
"reference": "749395fcd5f8f7530fe1f00dfa84eb22c83d94ea", "reference": "57b8f7efe40333cdb925700891c7d7465325d3b1",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -2582,7 +2582,7 @@
"laravel/framework": ">=10.17.0 <10.25.0" "laravel/framework": ">=10.17.0 <10.25.0"
}, },
"require-dev": { "require-dev": {
"illuminate/collections": "^10.0|^11.0", "illuminate/collections": "^10.0|^11.0|^12.0",
"mockery/mockery": "^1.5", "mockery/mockery": "^1.5",
"pestphp/pest": "^2.3|^3.4", "pestphp/pest": "^2.3|^3.4",
"phpstan/phpstan": "^1.11", "phpstan/phpstan": "^1.11",
@ -2612,9 +2612,9 @@
"description": "Add beautiful and user-friendly forms to your command-line applications.", "description": "Add beautiful and user-friendly forms to your command-line applications.",
"support": { "support": {
"issues": "https://github.com/laravel/prompts/issues", "issues": "https://github.com/laravel/prompts/issues",
"source": "https://github.com/laravel/prompts/tree/v0.3.3" "source": "https://github.com/laravel/prompts/tree/v0.3.5"
}, },
"time": "2024-12-30T15:53:31+00:00" "time": "2025-02-11T13:34:40+00:00"
}, },
{ {
"name": "laravel/sanctum", "name": "laravel/sanctum",

View File

@ -211,7 +211,7 @@
// 'ExampleClass' => App\Example\ExampleClass::class, // 'ExampleClass' => App\Example\ExampleClass::class,
])->toArray(), ])->toArray(),
'version' => '2.1.0', 'version' => '2.3.0',
'demo' => env('APP_DEMO', false), 'demo' => env('APP_DEMO', false),
]; ];

22
config/cli.php Normal file
View File

@ -0,0 +1,22 @@
<?php
return [
'providers' => [
Illuminate\Bus\BusServiceProvider::class,
Illuminate\Cache\CacheServiceProvider::class,
Illuminate\Foundation\Providers\ConsoleSupportServiceProvider::class,
Illuminate\Database\DatabaseServiceProvider::class,
Illuminate\Encryption\EncryptionServiceProvider::class,
Illuminate\Filesystem\FilesystemServiceProvider::class,
Illuminate\Foundation\Providers\FoundationServiceProvider::class,
Illuminate\Hashing\HashServiceProvider::class,
Illuminate\Mail\MailServiceProvider::class,
Illuminate\Notifications\NotificationServiceProvider::class,
Illuminate\Pagination\PaginationServiceProvider::class,
Illuminate\Pipeline\PipelineServiceProvider::class,
Illuminate\Queue\QueueServiceProvider::class,
Illuminate\Redis\RedisServiceProvider::class,
Illuminate\Translation\TranslationServiceProvider::class,
App\Providers\AppServiceProvider::class,
],
];

View File

@ -485,14 +485,6 @@
'post_max_size' => 'M', 'post_max_size' => 'M',
], ],
/*
* firewall
*/
'firewall_protocols_port' => [
'tcp' => '',
'udp' => '',
],
/* /*
* Disable these IPs for servers * Disable these IPs for servers
*/ */

View File

@ -62,7 +62,7 @@
'root' => storage_path('app/key-pairs'), 'root' => storage_path('app/key-pairs'),
], ],
'backups' => [ 'tmp' => [
'driver' => 'local', 'driver' => 'local',
'root' => sys_get_temp_dir(), 'root' => sys_get_temp_dir(),
], ],

View File

@ -0,0 +1,26 @@
<?php
namespace Database\Factories;
use App\Enums\CommandExecutionStatus;
use App\Models\Command;
use App\Models\CommandExecution;
use Illuminate\Database\Eloquent\Factories\Factory;
class CommandExecutionFactory extends Factory
{
protected $model = CommandExecution::class;
public function definition(): array
{
return [
'command_id' => Command::factory(),
'status' => $this->faker->randomElement([
CommandExecutionStatus::COMPLETED,
CommandExecutionStatus::FAILED,
CommandExecutionStatus::EXECUTING,
]),
'variables' => [],
];
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace Database\Factories;
use App\Models\Command;
use App\Models\Site;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Carbon;
class CommandFactory extends Factory
{
protected $model = Command::class;
public function definition(): array
{
return [
'name' => $this->faker->words(3, true),
'command' => 'php artisan '.$this->faker->word,
'created_at' => Carbon::now(),
'updated_at' => Carbon::now(),
'site_id' => Site::factory(),
];
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace Database\Factories;
use App\Models\File;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Carbon;
class FileFactory extends Factory
{
protected $model = File::class;
public function definition(): array
{
return [
'user_id' => $this->faker->randomNumber(), //
'server_id' => $this->faker->randomNumber(),
'server_user' => $this->faker->word(),
'path' => $this->faker->word(),
'type' => 'file',
'name' => $this->faker->name(),
'size' => $this->faker->randomNumber(),
'links' => $this->faker->randomNumber(),
'owner' => $this->faker->word(),
'group' => $this->faker->word(),
'date' => $this->faker->word(),
'permissions' => $this->faker->word(),
'created_at' => Carbon::now(),
'updated_at' => Carbon::now(),
];
}
}

View File

@ -12,6 +12,7 @@ class FirewallRuleFactory extends Factory
public function definition(): array public function definition(): array
{ {
return [ return [
'name' => $this->faker->word,
'type' => 'allow', 'type' => 'allow',
'protocol' => 'tcp', 'protocol' => 'tcp',
'port' => $this->faker->numberBetween(1, 65535), 'port' => $this->faker->numberBetween(1, 65535),

View File

@ -0,0 +1,33 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('files', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('user_id');
$table->unsignedBigInteger('server_id');
$table->string('server_user');
$table->string('path');
$table->string('type');
$table->string('name');
$table->unsignedBigInteger('size');
$table->unsignedBigInteger('links');
$table->string('owner');
$table->string('group');
$table->string('date');
$table->string('permissions');
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('files');
}
};

View File

@ -0,0 +1,39 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('firewall_rules', function (Blueprint $table) {
$table->string('name')->default('Undefined')->after('id');
$table->ipAddress('source')->default(null)->nullable()->change();
});
DB::statement("UPDATE firewall_rules SET name = UPPER(protocol) WHERE protocol IN ('ssh', 'http', 'https')");
DB::statement("UPDATE firewall_rules SET protocol = 'tcp' WHERE protocol IN ('ssh', 'http', 'https')");
DB::statement("UPDATE firewall_rules SET source = null WHERE source = '0.0.0.0'");
DB::statement("UPDATE firewall_rules SET mask = null WHERE mask = '0'");
}
/**
* Reverse the migrations.
*/
public function down(): void
{
DB::statement("UPDATE firewall_rules SET protocol = LOWER(name) WHERE protocol = 'tcp' AND LOWER(name) IN ('ssh', 'http', 'https')");
DB::statement("UPDATE firewall_rules SET source = '0.0.0.0' WHERE source is null");
DB::statement("UPDATE firewall_rules SET mask = '0' WHERE mask is null");
Schema::table('firewall_rules', function (Blueprint $table) {
$table->dropColumn('name');
$table->ipAddress('source')->default('0.0.0.0')->change();
});
}
};

View File

@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('commands', function (Blueprint $table) {
$table->id();
$table->unsignedInteger('site_id');
$table->string('name');
$table->text('command');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('commands');
}
};

View File

@ -0,0 +1,33 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('command_executions', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('command_id');
$table->unsignedInteger('server_id');
$table->unsignedBigInteger('user_id');
$table->unsignedBigInteger('server_log_id')->nullable();
$table->json('variables')->nullable();
$table->string('status');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('command_executions');
}
};

View File

@ -1,26 +1,22 @@
FROM ubuntu:22.04 FROM ubuntu:24.04
WORKDIR /var/www/html WORKDIR /var/www/html
ENV DEBIAN_FRONTEND noninteractive ENV DEBIAN_FRONTEND noninteractive
# upgrade # upgrade
RUN apt-get clean && apt-get update && apt-get update && apt-get upgrade -y && apt-get autoremove -y RUN apt-get update && apt-get upgrade -y && apt-get autoremove -y
# requirements # requirements
RUN apt-get install -y software-properties-common curl zip unzip gcc RUN apt-get install -y software-properties-common curl zip unzip gcc nginx \
cron gnupg gosu curl ca-certificates zip unzip supervisor libcap2-bin libpng-dev \
# nginx dnsutils librsvg2-bin fswatch wget openssh-client \
RUN apt-get install -y nginx
# php
RUN apt-get update \
&& apt-get install -y cron gnupg gosu curl ca-certificates zip unzip supervisor libcap2-bin libpng-dev \
python2 dnsutils librsvg2-bin fswatch wget openssh-client \
&& add-apt-repository ppa:ondrej/php -y \ && add-apt-repository ppa:ondrej/php -y \
&& apt-get update \ && apt-get update \
&& apt-get install -y php8.2 php8.2-fpm php8.2-mbstring php8.2-mcrypt php8.2-gd php8.2-xml \ && apt-get install -y php8.2 php8.2-fpm php8.2-mbstring php8.2-mcrypt php8.2-gd php8.2-xml \
php8.2-curl php8.2-gettext php8.2-zip php8.2-bcmath php8.2-soap php8.2-redis php8.2-sqlite3 php8.2-intl php8.2-curl php8.2-gettext php8.2-zip php8.2-bcmath php8.2-soap php8.2-redis php8.2-sqlite3 php8.2-intl
# php
COPY docker/php.ini /etc/php/8.2/cli/conf.d/99-vito.ini COPY docker/php.ini /etc/php/8.2/cli/conf.d/99-vito.ini
# composer # composer

54
docker/publish.sh Normal file
View File

@ -0,0 +1,54 @@
#!/bin/bash
BRANCH=""
TAGS=()
# Parse arguments
while [[ $# -gt 0 ]]; do
case "$1" in
--branch)
BRANCH="$2"
shift 2
;;
--tags)
IFS=',' read -r -a TAGS <<< "$2"
shift 2
;;
*)
echo "Unknown option: $1"
exit 1
;;
esac
done
# Validate inputs
if [ -z "$BRANCH" ]; then
echo "No branch provided. Use --branch <git_branch>"
exit 1
fi
if [ ${#TAGS[@]} -eq 0 ]; then
echo "No tags provided. Use --tags tag1,tag2,tag3"
exit 1
fi
# Clone the specified branch of the repo
rm -rf /tmp/vito
git clone --branch "$BRANCH" --depth 1 git@github.com:vitodeploy/vito.git /tmp/vito
cd /tmp/vito || exit
# Prepare tag arguments for docker buildx
TAG_ARGS=()
for TAG in "${TAGS[@]}"; do
# Trim whitespace to avoid invalid tag formatting
TAG_CLEANED=$(echo -n "$TAG" | xargs)
TAG_ARGS+=("-t" "vitodeploy/vito:$TAG_CLEANED")
done
# Build and push the image
docker buildx build . \
-f docker/Dockerfile \
"${TAG_ARGS[@]}" \
--platform linux/amd64,linux/arm64 \
--no-cache \
--push

646
package-lock.json generated
View File

@ -11,7 +11,7 @@
"apexcharts": "^3.44.2", "apexcharts": "^3.44.2",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"brace": "^0.11.1", "brace": "^0.11.1",
"laravel-vite-plugin": "^0.7.2", "laravel-vite-plugin": "^1.2.0",
"postcss": "^8.4.45", "postcss": "^8.4.45",
"postcss-nesting": "^13.0.0", "postcss-nesting": "^13.0.0",
"prettier": "^3.2.5", "prettier": "^3.2.5",
@ -19,7 +19,7 @@
"prettier-plugin-sh": "^0.14.0", "prettier-plugin-sh": "^0.14.0",
"prettier-plugin-tailwindcss": "^0.5.11", "prettier-plugin-tailwindcss": "^0.5.11",
"tailwindcss": "^3.4.10", "tailwindcss": "^3.4.10",
"vite": "^4.5.5" "vite": "^6.2.0"
} }
}, },
"node_modules/@alloc/quick-lru": { "node_modules/@alloc/quick-lru": {
@ -35,10 +35,27 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz",
"integrity": "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm": { "node_modules/@esbuild/android-arm": {
"version": "0.18.20", "version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.0.tgz",
"integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", "integrity": "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -49,13 +66,13 @@
"android" "android"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/android-arm64": { "node_modules/@esbuild/android-arm64": {
"version": "0.18.20", "version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz",
"integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", "integrity": "sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -66,13 +83,13 @@
"android" "android"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/android-x64": { "node_modules/@esbuild/android-x64": {
"version": "0.18.20", "version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.0.tgz",
"integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", "integrity": "sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -83,13 +100,13 @@
"android" "android"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/darwin-arm64": { "node_modules/@esbuild/darwin-arm64": {
"version": "0.18.20", "version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz",
"integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", "integrity": "sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -100,13 +117,13 @@
"darwin" "darwin"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/darwin-x64": { "node_modules/@esbuild/darwin-x64": {
"version": "0.18.20", "version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz",
"integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", "integrity": "sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -117,13 +134,13 @@
"darwin" "darwin"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/freebsd-arm64": { "node_modules/@esbuild/freebsd-arm64": {
"version": "0.18.20", "version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz",
"integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", "integrity": "sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -134,13 +151,13 @@
"freebsd" "freebsd"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/freebsd-x64": { "node_modules/@esbuild/freebsd-x64": {
"version": "0.18.20", "version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz",
"integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", "integrity": "sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -151,13 +168,13 @@
"freebsd" "freebsd"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/linux-arm": { "node_modules/@esbuild/linux-arm": {
"version": "0.18.20", "version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz",
"integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", "integrity": "sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -168,13 +185,13 @@
"linux" "linux"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/linux-arm64": { "node_modules/@esbuild/linux-arm64": {
"version": "0.18.20", "version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz",
"integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", "integrity": "sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -185,13 +202,13 @@
"linux" "linux"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/linux-ia32": { "node_modules/@esbuild/linux-ia32": {
"version": "0.18.20", "version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz",
"integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", "integrity": "sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@ -202,13 +219,13 @@
"linux" "linux"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/linux-loong64": { "node_modules/@esbuild/linux-loong64": {
"version": "0.18.20", "version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz",
"integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", "integrity": "sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==",
"cpu": [ "cpu": [
"loong64" "loong64"
], ],
@ -219,13 +236,13 @@
"linux" "linux"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/linux-mips64el": { "node_modules/@esbuild/linux-mips64el": {
"version": "0.18.20", "version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz",
"integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", "integrity": "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==",
"cpu": [ "cpu": [
"mips64el" "mips64el"
], ],
@ -236,13 +253,13 @@
"linux" "linux"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/linux-ppc64": { "node_modules/@esbuild/linux-ppc64": {
"version": "0.18.20", "version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz",
"integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", "integrity": "sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@ -253,13 +270,13 @@
"linux" "linux"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/linux-riscv64": { "node_modules/@esbuild/linux-riscv64": {
"version": "0.18.20", "version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz",
"integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", "integrity": "sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==",
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
@ -270,13 +287,13 @@
"linux" "linux"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/linux-s390x": { "node_modules/@esbuild/linux-s390x": {
"version": "0.18.20", "version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz",
"integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", "integrity": "sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==",
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
@ -287,13 +304,13 @@
"linux" "linux"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/linux-x64": { "node_modules/@esbuild/linux-x64": {
"version": "0.18.20", "version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz",
"integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", "integrity": "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -304,13 +321,30 @@
"linux" "linux"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz",
"integrity": "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
} }
}, },
"node_modules/@esbuild/netbsd-x64": { "node_modules/@esbuild/netbsd-x64": {
"version": "0.18.20", "version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz",
"integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", "integrity": "sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -321,13 +355,30 @@
"netbsd" "netbsd"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz",
"integrity": "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
} }
}, },
"node_modules/@esbuild/openbsd-x64": { "node_modules/@esbuild/openbsd-x64": {
"version": "0.18.20", "version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz",
"integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", "integrity": "sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -338,13 +389,13 @@
"openbsd" "openbsd"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/sunos-x64": { "node_modules/@esbuild/sunos-x64": {
"version": "0.18.20", "version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz",
"integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", "integrity": "sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -355,13 +406,13 @@
"sunos" "sunos"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/win32-arm64": { "node_modules/@esbuild/win32-arm64": {
"version": "0.18.20", "version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz",
"integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", "integrity": "sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -372,13 +423,13 @@
"win32" "win32"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/win32-ia32": { "node_modules/@esbuild/win32-ia32": {
"version": "0.18.20", "version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz",
"integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", "integrity": "sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@ -389,13 +440,13 @@
"win32" "win32"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@esbuild/win32-x64": { "node_modules/@esbuild/win32-x64": {
"version": "0.18.20", "version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz",
"integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", "integrity": "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -406,7 +457,7 @@
"win32" "win32"
], ],
"engines": { "engines": {
"node": ">=12" "node": ">=18"
} }
}, },
"node_modules/@isaacs/cliui": { "node_modules/@isaacs/cliui": {
@ -529,6 +580,272 @@
"node": ">=14" "node": ">=14"
} }
}, },
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.8.tgz",
"integrity": "sha512-q217OSE8DTp8AFHuNHXo0Y86e1wtlfVrXiAlwkIvGRQv9zbc6mE3sjIVfwI8sYUyNxwOg0j/Vm1RKM04JcWLJw==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.34.8.tgz",
"integrity": "sha512-Gigjz7mNWaOL9wCggvoK3jEIUUbGul656opstjaUSGC3eT0BM7PofdAJaBfPFWWkXNVAXbaQtC99OCg4sJv70Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.8.tgz",
"integrity": "sha512-02rVdZ5tgdUNRxIUrFdcMBZQoaPMrxtwSb+/hOfBdqkatYHR3lZ2A2EGyHq2sGOd0Owk80oV3snlDASC24He3Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.8.tgz",
"integrity": "sha512-qIP/elwR/tq/dYRx3lgwK31jkZvMiD6qUtOycLhTzCvrjbZ3LjQnEM9rNhSGpbLXVJYQ3rq39A6Re0h9tU2ynw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.34.8.tgz",
"integrity": "sha512-IQNVXL9iY6NniYbTaOKdrlVP3XIqazBgJOVkddzJlqnCpRi/yAeSOa8PLcECFSQochzqApIOE1GHNu3pCz+BDA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.34.8.tgz",
"integrity": "sha512-TYXcHghgnCqYFiE3FT5QwXtOZqDj5GmaFNTNt3jNC+vh22dc/ukG2cG+pi75QO4kACohZzidsq7yKTKwq/Jq7Q==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.34.8.tgz",
"integrity": "sha512-A4iphFGNkWRd+5m3VIGuqHnG3MVnqKe7Al57u9mwgbyZ2/xF9Jio72MaY7xxh+Y87VAHmGQr73qoKL9HPbXj1g==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.34.8.tgz",
"integrity": "sha512-S0lqKLfTm5u+QTxlFiAnb2J/2dgQqRy/XvziPtDd1rKZFXHTyYLoVL58M/XFwDI01AQCDIevGLbQrMAtdyanpA==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.8.tgz",
"integrity": "sha512-jpz9YOuPiSkL4G4pqKrus0pn9aYwpImGkosRKwNi+sJSkz+WU3anZe6hi73StLOQdfXYXC7hUfsQlTnjMd3s1A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.8.tgz",
"integrity": "sha512-KdSfaROOUJXgTVxJNAZ3KwkRc5nggDk+06P6lgi1HLv1hskgvxHUKZ4xtwHkVYJ1Rep4GNo+uEfycCRRxht7+Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-loongarch64-gnu": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.34.8.tgz",
"integrity": "sha512-NyF4gcxwkMFRjgXBM6g2lkT58OWztZvw5KkV2K0qqSnUEqCVcqdh2jN4gQrTn/YUpAcNKyFHfoOZEer9nwo6uQ==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.34.8.tgz",
"integrity": "sha512-LMJc999GkhGvktHU85zNTDImZVUCJ1z/MbAJTnviiWmmjyckP5aQsHtcujMjpNdMZPT2rQEDBlJfubhs3jsMfw==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.34.8.tgz",
"integrity": "sha512-xAQCAHPj8nJq1PI3z8CIZzXuXCstquz7cIOL73HHdXiRcKk8Ywwqtx2wrIy23EcTn4aZ2fLJNBB8d0tQENPCmw==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.34.8.tgz",
"integrity": "sha512-DdePVk1NDEuc3fOe3dPPTb+rjMtuFw89gw6gVWxQFAuEqqSdDKnrwzZHrUYdac7A7dXl9Q2Vflxpme15gUWQFA==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.8.tgz",
"integrity": "sha512-8y7ED8gjxITUltTUEJLQdgpbPh1sUQ0kMTmufRF/Ns5tI9TNMNlhWtmPKKHCU0SilX+3MJkZ0zERYYGIVBYHIA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.8.tgz",
"integrity": "sha512-SCXcP0ZpGFIe7Ge+McxY5zKxiEI5ra+GT3QRxL0pMMtxPfpyLAKleZODi1zdRHkz5/BhueUrYtYVgubqe9JBNQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.8.tgz",
"integrity": "sha512-YHYsgzZgFJzTRbth4h7Or0m5O74Yda+hLin0irAIobkLQFRQd1qWmnoVfwmKm9TXIZVAD0nZ+GEb2ICicLyCnQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.34.8.tgz",
"integrity": "sha512-r3NRQrXkHr4uWy5TOjTpTYojR9XmF0j/RYgKCef+Ag46FWUTltm5ziticv8LdNsDMehjJ543x/+TJAek/xBA2w==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.8.tgz",
"integrity": "sha512-U0FaE5O1BCpZSeE6gBl3c5ObhePQSfk9vDRToMmTkbhCOgW4jqvtS5LGyQ76L1fH8sM0keRp4uDTsbjiUyjk0g==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@tailwindcss/forms": { "node_modules/@tailwindcss/forms": {
"version": "0.5.10", "version": "0.5.10",
"resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.10.tgz", "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.10.tgz",
@ -558,6 +875,13 @@
"tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1"
} }
}, },
"node_modules/@types/estree": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
"integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
"dev": true,
"license": "MIT"
},
"node_modules/@vue/reactivity": { "node_modules/@vue/reactivity": {
"version": "3.1.5", "version": "3.1.5",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.1.5.tgz", "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.1.5.tgz",
@ -946,9 +1270,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/esbuild": { "node_modules/esbuild": {
"version": "0.18.20", "version": "0.25.0",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz",
"integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", "integrity": "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==",
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
@ -956,31 +1280,34 @@
"esbuild": "bin/esbuild" "esbuild": "bin/esbuild"
}, },
"engines": { "engines": {
"node": ">=12" "node": ">=18"
}, },
"optionalDependencies": { "optionalDependencies": {
"@esbuild/android-arm": "0.18.20", "@esbuild/aix-ppc64": "0.25.0",
"@esbuild/android-arm64": "0.18.20", "@esbuild/android-arm": "0.25.0",
"@esbuild/android-x64": "0.18.20", "@esbuild/android-arm64": "0.25.0",
"@esbuild/darwin-arm64": "0.18.20", "@esbuild/android-x64": "0.25.0",
"@esbuild/darwin-x64": "0.18.20", "@esbuild/darwin-arm64": "0.25.0",
"@esbuild/freebsd-arm64": "0.18.20", "@esbuild/darwin-x64": "0.25.0",
"@esbuild/freebsd-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.25.0",
"@esbuild/linux-arm": "0.18.20", "@esbuild/freebsd-x64": "0.25.0",
"@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-arm": "0.25.0",
"@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-arm64": "0.25.0",
"@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-ia32": "0.25.0",
"@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-loong64": "0.25.0",
"@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-mips64el": "0.25.0",
"@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-ppc64": "0.25.0",
"@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-riscv64": "0.25.0",
"@esbuild/linux-x64": "0.18.20", "@esbuild/linux-s390x": "0.25.0",
"@esbuild/netbsd-x64": "0.18.20", "@esbuild/linux-x64": "0.25.0",
"@esbuild/openbsd-x64": "0.18.20", "@esbuild/netbsd-arm64": "0.25.0",
"@esbuild/sunos-x64": "0.18.20", "@esbuild/netbsd-x64": "0.25.0",
"@esbuild/win32-arm64": "0.18.20", "@esbuild/openbsd-arm64": "0.25.0",
"@esbuild/win32-ia32": "0.18.20", "@esbuild/openbsd-x64": "0.25.0",
"@esbuild/win32-x64": "0.18.20" "@esbuild/sunos-x64": "0.25.0",
"@esbuild/win32-arm64": "0.25.0",
"@esbuild/win32-ia32": "0.25.0",
"@esbuild/win32-x64": "0.25.0"
} }
}, },
"node_modules/escalade": { "node_modules/escalade": {
@ -1255,20 +1582,23 @@
} }
}, },
"node_modules/laravel-vite-plugin": { "node_modules/laravel-vite-plugin": {
"version": "0.7.8", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-0.7.8.tgz", "resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-1.2.0.tgz",
"integrity": "sha512-HWYqpQYHR3kEQ1LsHX7gHJoNNf0bz5z5mDaHBLzS+PGLCTmYqlU5/SZyeEgObV7z7bC/cnStYcY9H1DI1D5Udg==", "integrity": "sha512-R0pJ+IcTVeqEMoKz/B2Ij57QVq3sFTABiFmb06gAwFdivbOgsUtuhX6N2MGLEArajrS3U5JbberzwOe7uXHMHQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"picocolors": "^1.0.0", "picocolors": "^1.0.0",
"vite-plugin-full-reload": "^1.0.5" "vite-plugin-full-reload": "^1.1.0"
},
"bin": {
"clean-orphaned-assets": "bin/clean.js"
}, },
"engines": { "engines": {
"node": ">=14" "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
}, },
"peerDependencies": { "peerDependencies": {
"vite": "^3.0.0 || ^4.0.0" "vite": "^5.0.0 || ^6.0.0"
} }
}, },
"node_modules/lilconfig": { "node_modules/lilconfig": {
@ -1546,9 +1876,9 @@
} }
}, },
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.5.1", "version": "8.5.3",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
"integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==", "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@ -1998,19 +2328,41 @@
} }
}, },
"node_modules/rollup": { "node_modules/rollup": {
"version": "3.29.5", "version": "4.34.8",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.5.tgz", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.34.8.tgz",
"integrity": "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==", "integrity": "sha512-489gTVMzAYdiZHFVA/ig/iYFllCcWFHMvUHI1rpFmkoUtRlQxqh6/yiNqnYibjMZ2b/+FUQwldG+aLsEt6bglQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": {
"@types/estree": "1.0.6"
},
"bin": { "bin": {
"rollup": "dist/bin/rollup" "rollup": "dist/bin/rollup"
}, },
"engines": { "engines": {
"node": ">=14.18.0", "node": ">=18.0.0",
"npm": ">=8.0.0" "npm": ">=8.0.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.34.8",
"@rollup/rollup-android-arm64": "4.34.8",
"@rollup/rollup-darwin-arm64": "4.34.8",
"@rollup/rollup-darwin-x64": "4.34.8",
"@rollup/rollup-freebsd-arm64": "4.34.8",
"@rollup/rollup-freebsd-x64": "4.34.8",
"@rollup/rollup-linux-arm-gnueabihf": "4.34.8",
"@rollup/rollup-linux-arm-musleabihf": "4.34.8",
"@rollup/rollup-linux-arm64-gnu": "4.34.8",
"@rollup/rollup-linux-arm64-musl": "4.34.8",
"@rollup/rollup-linux-loongarch64-gnu": "4.34.8",
"@rollup/rollup-linux-powerpc64le-gnu": "4.34.8",
"@rollup/rollup-linux-riscv64-gnu": "4.34.8",
"@rollup/rollup-linux-s390x-gnu": "4.34.8",
"@rollup/rollup-linux-x64-gnu": "4.34.8",
"@rollup/rollup-linux-x64-musl": "4.34.8",
"@rollup/rollup-win32-arm64-msvc": "4.34.8",
"@rollup/rollup-win32-ia32-msvc": "4.34.8",
"@rollup/rollup-win32-x64-msvc": "4.34.8",
"fsevents": "~2.3.2" "fsevents": "~2.3.2"
} }
}, },
@ -2480,41 +2832,48 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "4.5.9", "version": "6.2.0",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.9.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.0.tgz",
"integrity": "sha512-qK9W4xjgD3gXbC0NmdNFFnVFLMWSNiR3swj957yutwzzN16xF/E7nmtAyp1rT9hviDroQANjE4HK3H4WqWdFtw==", "integrity": "sha512-7dPxoo+WsT/64rDcwoOjk76XHj+TqNTIvHKcuMQ1k4/SeHDaQt5GFAeLYzrimZrMpn/O6DtdI03WUjdxuPM0oQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"esbuild": "^0.18.10", "esbuild": "^0.25.0",
"postcss": "^8.4.27", "postcss": "^8.5.3",
"rollup": "^3.27.1" "rollup": "^4.30.1"
}, },
"bin": { "bin": {
"vite": "bin/vite.js" "vite": "bin/vite.js"
}, },
"engines": { "engines": {
"node": "^14.18.0 || >=16.0.0" "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
}, },
"funding": { "funding": {
"url": "https://github.com/vitejs/vite?sponsor=1" "url": "https://github.com/vitejs/vite?sponsor=1"
}, },
"optionalDependencies": { "optionalDependencies": {
"fsevents": "~2.3.2" "fsevents": "~2.3.3"
}, },
"peerDependencies": { "peerDependencies": {
"@types/node": ">= 14", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
"jiti": ">=1.21.0",
"less": "*", "less": "*",
"lightningcss": "^1.21.0", "lightningcss": "^1.21.0",
"sass": "*", "sass": "*",
"sass-embedded": "*",
"stylus": "*", "stylus": "*",
"sugarss": "*", "sugarss": "*",
"terser": "^5.4.0" "terser": "^5.16.0",
"tsx": "^4.8.1",
"yaml": "^2.4.2"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
"@types/node": { "@types/node": {
"optional": true "optional": true
}, },
"jiti": {
"optional": true
},
"less": { "less": {
"optional": true "optional": true
}, },
@ -2524,6 +2883,9 @@
"sass": { "sass": {
"optional": true "optional": true
}, },
"sass-embedded": {
"optional": true
},
"stylus": { "stylus": {
"optional": true "optional": true
}, },
@ -2532,6 +2894,12 @@
}, },
"terser": { "terser": {
"optional": true "optional": true
},
"tsx": {
"optional": true
},
"yaml": {
"optional": true
} }
} }
}, },

View File

@ -13,7 +13,7 @@
"apexcharts": "^3.44.2", "apexcharts": "^3.44.2",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"brace": "^0.11.1", "brace": "^0.11.1",
"laravel-vite-plugin": "^0.7.2", "laravel-vite-plugin": "^1.2.0",
"postcss": "^8.4.45", "postcss": "^8.4.45",
"postcss-nesting": "^13.0.0", "postcss-nesting": "^13.0.0",
"prettier": "^3.2.5", "prettier": "^3.2.5",
@ -21,6 +21,6 @@
"prettier-plugin-sh": "^0.14.0", "prettier-plugin-sh": "^0.14.0",
"prettier-plugin-tailwindcss": "^0.5.11", "prettier-plugin-tailwindcss": "^0.5.11",
"tailwindcss": "^3.4.10", "tailwindcss": "^3.4.10",
"vite": "^4.5.5" "vite": "^6.2.0"
} }
} }

View File

@ -1,4 +1,4 @@
if ! echo '{{ $cron }}' | sudo -u {{ $user }} crontab -; then if ! echo '{!! $cron !!}' | sudo -u {{ $user }} crontab -; then
echo 'VITO_SSH_ERROR' && exit 1 echo 'VITO_SSH_ERROR' && exit 1
fi fi

View File

@ -0,0 +1,7 @@
if ! cd {{ $path }}; then
echo 'VITO_SSH_ERROR' && exit 1
fi
if ! git fetch origin; then
echo 'VITO_SSH_ERROR' && exit 1
fi

Some files were not shown because too many files have changed in this diff Show More