diff --git a/app/Actions/NodeJS/ChangeDefaultCli.php b/app/Actions/NodeJS/ChangeDefaultCli.php
new file mode 100644
index 0000000..16b41c0
--- /dev/null
+++ b/app/Actions/NodeJS/ChangeDefaultCli.php
@@ -0,0 +1,32 @@
+validate($server, $input);
+ $service = $server->nodejs($input['version']);
+ /** @var NodeJS $handler */
+ $handler = $service->handler();
+ $handler->setDefaultCli();
+ $server->defaultService('nodejs')->update(['is_default' => 0]);
+ $service->update(['is_default' => 1]);
+ $service->update(['status' => ServiceStatus::READY]);
+ }
+
+ public function validate(Server $server, array $input): void
+ {
+ if (! isset($input['version']) || ! in_array($input['version'], $server->installedNodejsVersions())) {
+ throw ValidationException::withMessages(
+ ['version' => __('This version is not installed')]
+ );
+ }
+ }
+}
diff --git a/app/Actions/NodeJS/InstallNewNodeJsVersion.php b/app/Actions/NodeJS/InstallNewNodeJsVersion.php
new file mode 100755
index 0000000..e5c0eba
--- /dev/null
+++ b/app/Actions/NodeJS/InstallNewNodeJsVersion.php
@@ -0,0 +1,45 @@
+ $server->id,
+ 'type' => 'nodejs',
+ 'type_data' => [],
+ 'name' => 'nodejs',
+ 'version' => $input['version'],
+ 'status' => ServiceStatus::INSTALLING,
+ 'is_default' => false,
+ ]);
+ $nodejs->save();
+
+ dispatch(function () use ($nodejs) {
+ $nodejs->handler()->install();
+ $nodejs->status = ServiceStatus::READY;
+ $nodejs->save();
+ })->catch(function () use ($nodejs) {
+ $nodejs->delete();
+ })->onConnection('ssh');
+ }
+
+ public static function rules(Server $server): array
+ {
+ return [
+ 'version' => [
+ 'required',
+ Rule::in(config('core.nodejs_versions')),
+ Rule::notIn(array_merge($server->installedNodejsVersions(), [NodeJS::NONE])),
+ ],
+ ];
+ }
+}
diff --git a/app/Actions/NodeJS/UninstallNodeJS.php b/app/Actions/NodeJS/UninstallNodeJS.php
new file mode 100755
index 0000000..8e5bd5a
--- /dev/null
+++ b/app/Actions/NodeJS/UninstallNodeJS.php
@@ -0,0 +1,53 @@
+validate($server, $input);
+
+ /** @var Service $nodejs */
+ $nodejs = $server->nodejs($input['version']);
+ $nodejs->status = ServiceStatus::UNINSTALLING;
+ $nodejs->save();
+
+ dispatch(function () use ($nodejs) {
+ $nodejs->handler()->uninstall();
+ $nodejs->delete();
+ })->catch(function () use ($nodejs) {
+ $nodejs->status = ServiceStatus::FAILED;
+ $nodejs->save();
+ })->onConnection('ssh');
+ }
+
+ /**
+ * @throws ValidationException
+ */
+ private function validate(Server $server, array $input): void
+ {
+ Validator::make($input, [
+ 'version' => 'required|string',
+ ])->validate();
+
+ if (! in_array($input['version'], $server->installedNodejsVersions())) {
+ throw ValidationException::withMessages(
+ ['version' => __('This version is not installed')]
+ );
+ }
+
+ $hasSite = $server->sites()->where('nodejs_version', $input['version'])->first();
+ if ($hasSite) {
+ throw ValidationException::withMessages(
+ ['version' => __('Cannot uninstall this version because some sites are using it!')]
+ );
+ }
+ }
+}
diff --git a/app/Enums/NodeJS.php b/app/Enums/NodeJS.php
new file mode 100644
index 0000000..fd6d308
--- /dev/null
+++ b/app/Enums/NodeJS.php
@@ -0,0 +1,32 @@
+services()->where('type', 'nodejs')->get(['version']);
+ foreach ($nodes as $node) {
+ $versions[] = $node->version;
+ }
+
+ return $versions;
+ }
+
public function type(): ServerType
{
$typeClass = config('core.server_types_class')[$this->type];
@@ -377,6 +388,15 @@ public function php(?string $version = null): ?Service
return $this->service('php', $version);
}
+ public function nodejs(?string $version = null): ?Service
+ {
+ if (! $version) {
+ return $this->defaultService('nodejs');
+ }
+
+ return $this->service('nodejs', $version);
+ }
+
public function memoryDatabase(?string $version = null): ?Service
{
if (! $version) {
diff --git a/app/Models/Site.php b/app/Models/Site.php
index 028668c..0852df8 100755
--- a/app/Models/Site.php
+++ b/app/Models/Site.php
@@ -43,6 +43,8 @@
* @property ?Ssl $activeSsl
* @property string $ssh_key_name
* @property ?SourceControl $sourceControl
+ *
+ * @TODO: Add nodejs_version column
*/
class Site extends AbstractModel
{
diff --git a/app/Policies/ServicePolicy.php b/app/Policies/ServicePolicy.php
index d14e896..eaddbd9 100644
--- a/app/Policies/ServicePolicy.php
+++ b/app/Policies/ServicePolicy.php
@@ -35,4 +35,29 @@ public function delete(User $user, Service $service): bool
{
return ($user->isAdmin() || $service->server->project->users->contains($user)) && $service->server->isReady();
}
+
+ public function start(User $user, Service $service): bool
+ {
+ return $this->update($user, $service) && $service->unit;
+ }
+
+ public function stop(User $user, Service $service): bool
+ {
+ return $this->update($user, $service) && $service->unit;
+ }
+
+ public function restart(User $user, Service $service): bool
+ {
+ return $this->update($user, $service) && $service->unit;
+ }
+
+ public function disable(User $user, Service $service): bool
+ {
+ return $this->update($user, $service) && $service->unit;
+ }
+
+ public function enable(User $user, Service $service): bool
+ {
+ return $this->update($user, $service) && $service->unit;
+ }
}
diff --git a/app/SSH/Services/NodeJS/NodeJS.php b/app/SSH/Services/NodeJS/NodeJS.php
new file mode 100644
index 0000000..1413f6c
--- /dev/null
+++ b/app/SSH/Services/NodeJS/NodeJS.php
@@ -0,0 +1,77 @@
+ [
+ 'required',
+ Rule::in(config('core.nodejs_versions')),
+ Rule::notIn([\App\Enums\NodeJS::NONE]),
+ Rule::unique('services', 'version')
+ ->where('type', 'nodejs')
+ ->where('server_id', $this->service->server_id),
+ ],
+ ];
+ }
+
+ public function deletionRules(): array
+ {
+ return [
+ 'service' => [
+ function (string $attribute, mixed $value, Closure $fail) {
+ $hasSite = $this->service->server->sites()
+ ->where('nodejs_version', $this->service->version)
+ ->exists();
+ if ($hasSite) {
+ $fail('Some sites are using this NodeJS version.');
+ }
+ },
+ ],
+ ];
+ }
+
+ public function install(): void
+ {
+ $server = $this->service->server;
+ $server->ssh()->exec(
+ $this->getScript('install-nodejs.sh', [
+ 'version' => $this->service->version,
+ 'user' => $server->getSshUser(),
+ ]),
+ 'install-nodejs-'.$this->service->version
+ );
+ $this->service->server->os()->cleanup();
+ }
+
+ public function uninstall(): void
+ {
+ $this->service->server->ssh()->exec(
+ $this->getScript('uninstall-nodejs.sh', [
+ 'version' => $this->service->version,
+ ]),
+ 'uninstall-nodejs-'.$this->service->version
+ );
+ $this->service->server->os()->cleanup();
+ }
+
+ public function setDefaultCli(): void
+ {
+ $this->service->server->ssh()->exec(
+ $this->getScript('change-default-nodejs.sh', [
+ 'version' => $this->service->version,
+ ]),
+ 'change-default-nodejs'
+ );
+ }
+}
diff --git a/app/SSH/Services/NodeJS/scripts/change-default-nodejs.sh b/app/SSH/Services/NodeJS/scripts/change-default-nodejs.sh
new file mode 100755
index 0000000..fa8b39b
--- /dev/null
+++ b/app/SSH/Services/NodeJS/scripts/change-default-nodejs.sh
@@ -0,0 +1,13 @@
+export NVM_DIR="$HOME/.nvm"
+[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
+
+if ! nvm alias default __version__; then
+ echo 'VITO_SSH_ERROR' && exit 1
+fi
+
+if ! nvm use default; then
+ echo 'VITO_SSH_ERROR' && exit 1
+fi
+
+echo "Default Node.js is now:"
+node -v
diff --git a/app/SSH/Services/NodeJS/scripts/install-nodejs.sh b/app/SSH/Services/NodeJS/scripts/install-nodejs.sh
new file mode 100755
index 0000000..53a8998
--- /dev/null
+++ b/app/SSH/Services/NodeJS/scripts/install-nodejs.sh
@@ -0,0 +1,68 @@
+# Download NVM, if not already downloaded
+if [ ! -d "$HOME/.nvm" ]; then
+ if ! git clone https://github.com/nvm-sh/nvm.git "$HOME/.nvm"; then
+ echo 'VITO_SSH_ERROR' && exit 1
+ fi
+fi
+
+# Checkout the latest stable version of NVM
+if ! git -C "$HOME/.nvm" checkout v0.40.1; then
+ echo 'VITO_SSH_ERROR' && exit 1
+fi
+
+# Load NVM
+export NVM_DIR="$HOME/.nvm"
+[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
+
+# Define the NVM initialization script
+NVM_INIT='
+export NVM_DIR="$HOME/.nvm"
+[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
+'
+
+# List of potential configuration files
+CONFIG_FILES=("$HOME/.bash_profile" "$HOME/.bash_login" "$HOME/.profile")
+
+# Flag to track if at least one file exists
+FILE_EXISTS=false
+
+# Loop through each configuration file and check if it exists
+for config_file in "${CONFIG_FILES[@]}"; do
+ if [ -f "$config_file" ]; then
+ FILE_EXISTS=true
+ # Check if the NVM initialization is already present
+ if ! grep -q 'export NVM_DIR="$HOME/.nvm"' "$config_file"; then
+ echo "Adding NVM initialization to $config_file"
+ echo "$NVM_INIT" >> "$config_file"
+ else
+ echo "NVM initialization already exists in $config_file"
+ fi
+ fi
+done
+
+# If no file exists, fallback to .profile
+if [ "$FILE_EXISTS" = false ]; then
+ FALLBACK_FILE="$HOME/.profile"
+ echo "No configuration files found. Creating $FALLBACK_FILE and adding NVM initialization."
+ echo "$NVM_INIT" >> "$FALLBACK_FILE"
+fi
+
+echo "NVM initialization process completed."
+
+# Install NVM if not already installed
+if ! command -v nvm > /dev/null 2>&1; then
+ if ! bash "$HOME/.nvm/install.sh"; then
+ echo 'VITO_SSH_ERROR' && exit 1
+ fi
+fi
+
+# Install the requested Node.js version
+if ! nvm install __version__; then
+ echo 'VITO_SSH_ERROR' && exit 1
+fi
+
+echo "Node.js version __version__ installed successfully!"
+
+echo "Node version:" && node -v
+echo "NPM version:" && npm -v
+echo "NPX version:" && npx -v
diff --git a/app/SSH/Services/NodeJS/scripts/uninstall-nodejs.sh b/app/SSH/Services/NodeJS/scripts/uninstall-nodejs.sh
new file mode 100755
index 0000000..adbed0b
--- /dev/null
+++ b/app/SSH/Services/NodeJS/scripts/uninstall-nodejs.sh
@@ -0,0 +1,6 @@
+export NVM_DIR="$HOME/.nvm"
+[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
+
+if ! nvm uninstall __version__; then
+ echo 'VITO_SSH_ERROR' && exit 1
+fi
diff --git a/app/Web/Pages/Servers/NodeJS/Index.php b/app/Web/Pages/Servers/NodeJS/Index.php
new file mode 100644
index 0000000..6387374
--- /dev/null
+++ b/app/Web/Pages/Servers/NodeJS/Index.php
@@ -0,0 +1,66 @@
+authorize('viewAny', [Service::class, $this->server]);
+ }
+
+ public function getWidgets(): array
+ {
+ return [
+ [NodeJSList::class, ['server' => $this->server]],
+ ];
+ }
+
+ protected function getHeaderActions(): array
+ {
+ $installedNodeVersions = $this->server->installedNodejsVersions();
+
+ return [
+ Action::make('install')
+ ->authorize(fn () => auth()->user()?->can('create', [Service::class, $this->server]))
+ ->label('Install Node')
+ ->icon('heroicon-o-archive-box-arrow-down')
+ ->modalWidth(MaxWidth::Large)
+ ->form([
+ Select::make('version')
+ ->options(
+ collect(config('core.nodejs_versions'))
+ ->filter(fn ($version) => ! in_array($version, array_merge($installedNodeVersions, [NodeJS::NONE])))
+ ->mapWithKeys(fn ($version) => [$version => $version])
+ ->toArray()
+ )
+ ->rules(InstallNewNodeJsVersion::rules($this->server)['version']),
+ ])
+ ->modalSubmitActionLabel('Install')
+ ->action(function (array $data) {
+ app(InstallNewNodeJsVersion::class)->install($this->server, $data);
+
+ Notification::make()
+ ->success()
+ ->title('Installing Node...')
+ ->send();
+
+ $this->dispatch('$refresh');
+ }),
+ ];
+ }
+}
diff --git a/app/Web/Pages/Servers/NodeJS/Widgets/NodeJSList.php b/app/Web/Pages/Servers/NodeJS/Widgets/NodeJSList.php
new file mode 100644
index 0000000..262a0be
--- /dev/null
+++ b/app/Web/Pages/Servers/NodeJS/Widgets/NodeJSList.php
@@ -0,0 +1,117 @@
+where('type', 'nodejs')->where('server_id', $this->server->id);
+ }
+
+ protected function getTableColumns(): array
+ {
+ return [
+ TextColumn::make('version')
+ ->sortable(),
+ TextColumn::make('status')
+ ->label('Status')
+ ->badge()
+ ->color(fn (Service $service) => Service::$statusColors[$service->status])
+ ->sortable(),
+ TextColumn::make('is_default')
+ ->label('Default Cli')
+ ->badge()
+ ->color(fn (Service $service) => $service->is_default ? 'primary' : 'gray')
+ ->state(fn (Service $service) => $service->is_default ? 'Yes' : 'No')
+ ->sortable(),
+ TextColumn::make('created_at')
+ ->label('Installed At')
+ ->formatStateUsing(fn ($record) => $record->created_at_by_timezone),
+ ];
+ }
+
+ /**
+ * @throws Exception
+ */
+ public function table(Table $table): Table
+ {
+ return $table
+ ->heading(null)
+ ->query($this->getTableQuery())
+ ->columns($this->getTableColumns())
+ ->actions([
+ ActionGroup::make([
+ $this->defaultNodeJsCliAction(),
+ $this->uninstallAction(),
+ ]),
+ ]);
+ }
+
+ private function defaultNodeJsCliAction(): Action
+ {
+ return Action::make('default-nodejs-cli')
+ ->authorize(fn (Service $nodejs) => auth()->user()?->can('update', $nodejs))
+ ->label('Make Default CLI')
+ ->hidden(fn (Service $service) => $service->is_default)
+ ->action(function (Service $service) {
+ try {
+ app(ChangeDefaultCli::class)->change($this->server, ['version' => $service->version]);
+
+ Notification::make()
+ ->success()
+ ->title('Default NodeJS CLI changed!')
+ ->send();
+ } catch (Exception $e) {
+ Notification::make()
+ ->danger()
+ ->title($e->getMessage())
+ ->send();
+
+ throw $e;
+ }
+
+ $this->dispatch('$refresh');
+ });
+ }
+
+ private function uninstallAction(): Action
+ {
+ return Action::make('uninstall')
+ ->authorize(fn (Service $nodejs) => auth()->user()?->can('update', $nodejs))
+ ->label('Uninstall')
+ ->color('danger')
+ ->requiresConfirmation()
+ ->action(function (Service $service) {
+ try {
+ app(Uninstall::class)->uninstall($service);
+ } catch (Exception $e) {
+ Notification::make()
+ ->danger()
+ ->title($e->getMessage())
+ ->send();
+
+ throw $e;
+ }
+
+ $this->dispatch('$refresh');
+ });
+ }
+}
diff --git a/app/Web/Pages/Servers/Page.php b/app/Web/Pages/Servers/Page.php
index 01b9e3b..c082cda 100644
--- a/app/Web/Pages/Servers/Page.php
+++ b/app/Web/Pages/Servers/Page.php
@@ -18,6 +18,7 @@
use App\Web\Pages\Servers\Firewall\Index as FirewallIndex;
use App\Web\Pages\Servers\Logs\Index as LogsIndex;
use App\Web\Pages\Servers\Metrics\Index as MetricsIndex;
+use App\Web\Pages\Servers\NodeJS\Index as NodeJsIndex;
use App\Web\Pages\Servers\PHP\Index as PHPIndex;
use App\Web\Pages\Servers\Services\Index as ServicesIndex;
use App\Web\Pages\Servers\Settings as ServerSettings;
@@ -60,11 +61,18 @@ public function getSubNavigation(): array
if (auth()->user()->can('viewAny', [Service::class, $this->server])) {
$items[] = NavigationItem::make(PHPIndex::getNavigationLabel())
- ->icon('heroicon-o-code-bracket')
+ ->icon('icon-php-alt')
->isActiveWhen(fn () => request()->routeIs(PHPIndex::getRouteName().'*'))
->url(PHPIndex::getUrl(parameters: ['server' => $this->server]));
}
+ if (auth()->user()->can('viewAny', [Service::class, $this->server])) {
+ $items[] = NavigationItem::make(NodeJsIndex::getNavigationLabel())
+ ->icon('icon-nodejs-alt')
+ ->isActiveWhen(fn () => request()->routeIs(NodeJsIndex::getRouteName().'*'))
+ ->url(NodeJsIndex::getUrl(parameters: ['server' => $this->server]));
+ }
+
if (auth()->user()->can('viewAny', [FirewallRule::class, $this->server])) {
$items[] = NavigationItem::make(FirewallIndex::getNavigationLabel())
->icon('heroicon-o-fire')
diff --git a/app/Web/Pages/Servers/Services/Widgets/ServicesList.php b/app/Web/Pages/Servers/Services/Widgets/ServicesList.php
index 095bfdf..e4d9a50 100644
--- a/app/Web/Pages/Servers/Services/Widgets/ServicesList.php
+++ b/app/Web/Pages/Servers/Services/Widgets/ServicesList.php
@@ -75,7 +75,7 @@ public function table(Table $table): Table
private function serviceAction(string $type, string $icon): Action
{
return Action::make($type)
- ->authorize(fn (Service $service) => auth()->user()?->can('update', $service))
+ ->authorize(fn (Service $service) => auth()->user()?->can($type, $service))
->label(ucfirst($type).' Service')
->icon($icon)
->action(function (Service $service) use ($type) {
diff --git a/config/core.php b/config/core.php
index 934214c..0ca948b 100755
--- a/config/core.php
+++ b/config/core.php
@@ -40,6 +40,19 @@
\App\Enums\PHP::V83,
\App\Enums\PHP::V84,
],
+ 'nodejs_versions' => [
+ \App\Enums\NodeJS::NONE,
+ \App\Enums\NodeJS::V4,
+ \App\Enums\NodeJS::V6,
+ \App\Enums\NodeJS::V8,
+ \App\Enums\NodeJS::V10,
+ \App\Enums\NodeJS::V12,
+ \App\Enums\NodeJS::V14,
+ \App\Enums\NodeJS::V16,
+ \App\Enums\NodeJS::V18,
+ \App\Enums\NodeJS::V20,
+ \App\Enums\NodeJS::V22,
+ ],
'databases' => [
\App\Enums\Database::NONE,
\App\Enums\Database::MYSQL57,
@@ -162,6 +175,7 @@
'postgresql' => 'database',
'redis' => 'memory_database',
'php' => 'php',
+ 'nodejs' => 'nodejs',
'ufw' => 'firewall',
'supervisor' => 'process_manager',
'vito-agent' => 'monitoring',
@@ -174,6 +188,7 @@
'postgresql' => \App\SSH\Services\Database\Postgresql::class,
'redis' => \App\SSH\Services\Redis\Redis::class,
'php' => \App\SSH\Services\PHP\PHP::class,
+ 'nodejs' => \App\SSH\Services\NodeJS\NodeJS::class,
'ufw' => \App\SSH\Services\Firewall\Ufw::class,
'supervisor' => \App\SSH\Services\ProcessManager\Supervisor::class,
'vito-agent' => \App\SSH\Services\Monitoring\VitoAgent\VitoAgent::class,
@@ -204,6 +219,18 @@
'redis' => [
'latest',
],
+ 'nodejs' => [
+ '4',
+ '6',
+ '8',
+ '10',
+ '12',
+ '14',
+ '16',
+ '18',
+ '20',
+ '22',
+ ],
'php' => [
'5.6',
'7.0',
diff --git a/resources/svg/nodejs-alt.svg b/resources/svg/nodejs-alt.svg
new file mode 100644
index 0000000..f05a36b
--- /dev/null
+++ b/resources/svg/nodejs-alt.svg
@@ -0,0 +1,7 @@
+
+
diff --git a/resources/svg/nodejs.svg b/resources/svg/nodejs.svg
new file mode 100644
index 0000000..2163bb7
--- /dev/null
+++ b/resources/svg/nodejs.svg
@@ -0,0 +1,2 @@
+
+
diff --git a/resources/svg/php-alt.svg b/resources/svg/php-alt.svg
new file mode 100644
index 0000000..1cf1246
--- /dev/null
+++ b/resources/svg/php-alt.svg
@@ -0,0 +1,5 @@
+
+
diff --git a/tests/Feature/NodeJSTest.php b/tests/Feature/NodeJSTest.php
new file mode 100644
index 0000000..f5ed652
--- /dev/null
+++ b/tests/Feature/NodeJSTest.php
@@ -0,0 +1,94 @@
+actingAs($this->user);
+
+ Livewire::test(Index::class, ['server' => $this->server])
+ ->callAction('install', [
+ 'version' => NodeJS::V16,
+ ])
+ ->assertSuccessful();
+
+ $this->assertDatabaseHas('services', [
+ 'server_id' => $this->server->id,
+ 'type' => 'nodejs',
+ 'version' => NodeJS::V16,
+ 'status' => ServiceStatus::READY,
+ ]);
+ }
+
+ public function test_uninstall_nodejs(): void
+ {
+ SSH::fake();
+
+ $this->actingAs($this->user);
+
+ $php = new Service([
+ 'server_id' => $this->server->id,
+ 'type' => 'nodejs',
+ 'type_data' => [],
+ 'name' => 'nodejs',
+ 'version' => NodeJS::V16,
+ 'status' => ServiceStatus::READY,
+ 'is_default' => true,
+ ]);
+ $php->save();
+
+ Livewire::test(NodeJSList::class, [
+ 'server' => $this->server,
+ ])
+ ->callTableAction('uninstall', $php->id)
+ ->assertSuccessful();
+
+ $this->assertDatabaseMissing('services', [
+ 'id' => $php->id,
+ ]);
+ }
+
+ public function test_change_default_nodejs_cli(): void
+ {
+ SSH::fake();
+
+ $this->actingAs($this->user);
+
+ /** @var Service $service */
+ $service = Service::factory()->create([
+ 'server_id' => $this->server->id,
+ 'type' => 'nodejs',
+ 'type_data' => [],
+ 'name' => 'nodejs',
+ 'version' => NodeJS::V16,
+ 'status' => ServiceStatus::READY,
+ 'is_default' => false,
+ ]);
+
+ Livewire::test(NodeJSList::class, [
+ 'server' => $this->server,
+ ])
+ ->callTableAction('default-nodejs-cli', $service->id)
+ ->assertSuccessful();
+
+ $service->refresh();
+
+ $this->assertTrue($service->is_default);
+ }
+}
diff --git a/tests/Feature/ServicesTest.php b/tests/Feature/ServicesTest.php
index 0cc55f8..4227c55 100644
--- a/tests/Feature/ServicesTest.php
+++ b/tests/Feature/ServicesTest.php
@@ -318,6 +318,11 @@ public static function installData(): array
'php',
'7.4',
],
+ [
+ 'nodejs',
+ 'nodejs',
+ '16',
+ ],
[
'supervisor',
'process_manager',