diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..6537ca4 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false + +[*.{yml,yaml}] +indent_size = 2 diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..c465c63 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,23 @@ + + +# Please see the documentation for all configuration options: +# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "composer" + directory: "/" + schedule: + interval: "weekly" + day: tuesday + time: "12:00" + labels: + - "dependencies" + open-pull-requests-limit: 10 + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: weekly + day: tuesday + time: "12:00" diff --git a/.github/workflows/php-cs-fixer.yml b/.github/workflows/php-cs-fixer.yml new file mode 100644 index 0000000..2385b2a --- /dev/null +++ b/.github/workflows/php-cs-fixer.yml @@ -0,0 +1,28 @@ +name: Check & fix styling + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + php-cs-fixer: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 +# with: +# ref: ${{ github.head_ref }} +# token: ${{ secrets.PAT }} + + - name: Run PHP CS Fixer + uses: docker://oskarstark/php-cs-fixer-ga + with: + args: --config=ruleset-php_cs.php --allow-risky=yes --show-progress=dots --diff --dry-run +# +# - name: Commit changes +# uses: stefanzweifel/git-auto-commit-action@v4 +# with: +# commit_message: Apply php-cs-fixer changes diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml new file mode 100644 index 0000000..5319997 --- /dev/null +++ b/.github/workflows/phpstan.yml @@ -0,0 +1,26 @@ +name: PHPStan + +on: + push: + paths: + - '**.php' + - 'ruleset-phpstan.neon' + +jobs: + phpstan: + name: phpstan + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.1' + coverage: none + + - name: Install composer dependencies + uses: ramsey/composer-install@v3 + + - name: Run PHPStan + run: ./vendor/bin/phpstan --error-format=github analyse -c ruleset-phpstan.neon -vvv diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml new file mode 100644 index 0000000..0c58cc5 --- /dev/null +++ b/.github/workflows/phpunit.yml @@ -0,0 +1,76 @@ +name: phpunit + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + test: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: true + matrix: + os: [ubuntu-latest] + php: [8.1, 8.2, 8.3] + laravel: [10.*, 11.*] + include: + - laravel: 10.* + testbench: 8.* + - laravel: 11.* + testbench: 9.* + exclude: + - laravel: 11.* + php: 8.1 + + + name: PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }} - ${{ matrix.os }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, fileinfo + coverage: pcov + + - name: Setup problem matchers + run: | + echo "::add-matcher::${{ runner.tool_cache }}/php.json" + echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" + + - name: Get composer cache directory + id: composer-cache + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + + - name: Cache dependencies + uses: actions/cache@v4 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} + restore-keys: ${{ runner.os }}-composer- + + + + - name: Install composer dependencies + run: | + composer --version + composer require "laravel/framework:${{ matrix.laravel }}" --no-interaction --no-update + composer require "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update --dev + composer update --prefer-dist --no-interaction --no-suggest --dev + composer dump + + + - name: Execute tests + run: vendor/bin/phpunit + + - name: Upload coverage results to Coveralls + env: + COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + composer global require php-coveralls/php-coveralls + php-coveralls --coverage_clover=build/logs/clover.xml -v diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..767621a --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/vendor +**/*.cache +/build + +.idea diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..9eca9c3 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) GeoSot + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..2685104 --- /dev/null +++ b/README.md @@ -0,0 +1,100 @@ +# Filament Env Editor + +[![Latest Version on Packagist](https://img.shields.io/packagist/v/saade/filament-laravel-log.svg?style=flat-square)](https://packagist.org/packages/geo-sot/filament-env-editor) +[![Total Downloads](https://img.shields.io/packagist/dt/geo-sot/filament-env-editor.svg?style=flat-square)](https://packagist.org/packages/geo-sot/filament-env-editor) + +

+ Banner +

+ +# Features + + + +
+ +## Installation + +You can install the package via composer: + +```bash +composer require geo-sot/filament-env-editor +``` + +## Usage + +Add the `GeoSot\FilamentEnvEditor\EnvEditorPlugin` to your panel config. + +```php +use GeoSot\FilamentEnvEditor\EnvEditorPlugin; + +class AdminPanelProvider extends PanelProvider +{ + public function panel(Panel $panel): Panel + { + return $panel + // ... + ->plugin( + EnvEditorPlugin::make() + ); + } +} +``` + +## Configuration + +### Customizing the navigation item + +```php +EnvEditorPlugin::make() + ->navigationGroup('System Tools') + ->navigationLabel('My Env') + ->navigationIcon('heroicon-o-cog-8-tooth') + ->navigationSort(1) + ->slug('env-editor') +``` + + +### Authorization +If you would like to prevent certain users from accessing the logs page, you should add a `authorize` callback in the EnvEditorPlugin chain. + +```php +EnvEditorPlugin::make() + ->authorize( + fn () => auth()->user()->isAdmin() + ) +``` + +### Customizing the log page + +To customize the "env-editor" page, you can extend the `GeoSot\FilamentEnvEditor\Pages\ViewEnv` page and override its methods. + +```php +use GeoSot\FilamentEnvEditor\Pages\ViewEnv as BaseViewEnvEditor; + +class ViewEnv extends BaseViewEnvEditor +{ + // Your implementation +} +``` + +```php +use App\Filament\Pages\ViewEnv; + +EnvEditorPlugin::make() + ->viewLog(ViewEnv::class) +``` + + +## Changelog + +Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. + +## Contributing + +Please see [CONTRIBUTING](.github/CONTRIBUTING.md) for details. + + +## License + +The MIT License (MIT). Please see [License File](LICENSE.md) for more information. diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..c57fc9e --- /dev/null +++ b/composer.json @@ -0,0 +1,70 @@ +{ + "name": "geo-sot/filament-env-editor", + "description": "Access .env file though Filament admin panel", + "keywords": [ + "geo-sot", + "laravel", + "laravel-env-editor", + "EnvEditor", + "filament-env-editor" + ], + "homepage": "https://github.com/GeoSot/filament-env-editor", + "support": { + "issues": "https://github.com/GeoSot/filament-env-editor/issues", + "source": "https://github.com/GeoSot/filament-env-editor" + }, + "license": "MIT", + "authors": [ + { + "name": "Geo Sot", + "email": "geo.sotis@gmail.com" + } + ], + "require": { + "php": ">=8.1", + "filament/filament": "^3.0", + "illuminate/contracts": "^10.0|^11.0", + "spatie/laravel-package-tools": "^1.15.0", + "geo-sot/laravel-env-editor": "dev-main" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3", + "larastan/larastan": "^2", + "orchestra/testbench": ">=9", + "phpstan/phpstan-deprecation-rules": "^1.0", + "phpstan/phpstan-phpunit": "^1.0" + + }, + "autoload": { + "psr-4": { + "GeoSot\\FilamentEnvEditor\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Saade\\FilamentLaravelLog\\Tests\\": "tests/" + } + }, + "scripts": { + "phpstan": "php --version && php vendor/bin/phpstan --version && php -d memory_limit=1G vendor/bin/phpstan analyse -c ruleset-phpstan.neon -vvv", + "cs": "./vendor/bin/php-cs-fixer fix -vvv --show-progress=dots --config=ruleset-php_cs.php", + "test": "./vendor/bin/phpunit", + "test-all": [ + "@test", + "@phpstan", + "@cs" + ] + }, + "config": { + "sort-packages": true + }, + "extra": { + "laravel": { + "providers": [ + "GeoSot\\FilamentEnvEditor\\ServiceProvider" + ] + } + }, + "minimum-stability": "dev", + "prefer-stable": true +} diff --git a/resources/lang/en/filament-env-editor.php b/resources/lang/en/filament-env-editor.php new file mode 100644 index 0000000..2334614 --- /dev/null +++ b/resources/lang/en/filament-env-editor.php @@ -0,0 +1,54 @@ + [ + 'group' => 'System', + 'label' => '.Env Editor', + ], + + 'page' => [ + 'title' => '.Env Editor', + + 'form' => [ + ], + 'actions' => [ + 'add' => [ + 'title' => 'Add new Entry', + 'modalHeading' => 'Add new Entry', + 'success' => [ + 'title' => 'Key ":Name", was successfully written', + ], + 'form' => [ + 'fields' => [ + 'key' => 'key', + 'value' => 'value', + 'index' => 'Insert After existing key (optional)', + ], + 'helpText' => [ + 'index' => 'In case you need to put this new entry, after an existing one, you may pick one of the existing key ', + ], + ], + ], + 'edit' => [ + 'tooltip' => 'Edit Entry ":name"', + 'modal' => [ + 'text' => 'Edit Entry', + ], + ], + 'delete' => [ + 'tooltip' => 'Remove the ":name" entry', + 'confirm' => [ + 'title' => 'You are going to permanently remove ":name". Are you sure of this removal?', + ], + ], + 'cache' => [ + 'title' => 'Cache all', + 'tooltip' => 'It runs "artisan optimize" in order to recreate your caches', + ], + 'clear-cache' => [ + 'title' => 'Clear caches', + 'tooltip' => 'Sometimes laravel caches ENV variables, so you need to clear all caches ("artisan optimize:clear"), in order to rer-ead the .env change', + ], + ], + ], +]; diff --git a/resources/views/.gitkeep b/resources/views/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/resources/views/view-editor.blade.php b/resources/views/view-editor.blade.php new file mode 100644 index 0000000..e23012d --- /dev/null +++ b/resources/views/view-editor.blade.php @@ -0,0 +1,12 @@ + + + {{ $this->form }} + + + + + + diff --git a/ruleset-php_cs.php b/ruleset-php_cs.php new file mode 100644 index 0000000..0d0e800 --- /dev/null +++ b/ruleset-php_cs.php @@ -0,0 +1,31 @@ + true, + 'php_unit_method_casing' => ['case' => 'snake_case'], + 'elseif' => true, + 'phpdoc_align' => ['align' => 'left'] +]; + +$dirsToCheck = [ + __DIR__.'/src', + __DIR__.'/resources', + __DIR__.'/tests' +]; + +$finder = Finder::create() + ->in(array_filter($dirsToCheck, 'file_exists')) + ->exclude(['vendor']) + ->name('*.php') + ->notName('*.blade.php') + ->ignoreDotFiles(true) + ->ignoreVCS(true); + +return (new Config()) + ->setFinder($finder) + ->setRules($rules) + ->setRiskyAllowed(true) + ->setUsingCache(true); diff --git a/ruleset-phpstan.neon b/ruleset-phpstan.neon new file mode 100644 index 0000000..d7c219f --- /dev/null +++ b/ruleset-phpstan.neon @@ -0,0 +1,11 @@ +includes: + - ./vendor/larastan/larastan/extension.neon +parameters: + paths: + - src + level: 6 + reportStaticMethodSignatures: true + parallel: + jobSize: 20 + maximumNumberOfProcesses: 32 + minimumNumberOfJobsPerProcess: 2 diff --git a/src/EnvEditorPlugin.php b/src/EnvEditorPlugin.php new file mode 100644 index 0000000..3fa6fd1 --- /dev/null +++ b/src/EnvEditorPlugin.php @@ -0,0 +1,135 @@ +getId()); + } + + public function register(Panel $panel): void + { + $panel + ->pages([ + $this->viewPage, + ]); + } + + public function boot(Panel $panel): void + { + } + + public function authorize(bool|\Closure $callback = true): static + { + $this->authorizeUsing = $callback; + + return $this; + } + + public function isAuthorized(): bool + { + return true === $this->evaluate($this->authorizeUsing); + } + + /** + * @param class-string $page + */ + public function viewPage(string $page): static + { + $this->viewPage = $page; + + return $this; + } + + public function navigationGroup(string|\Closure|null $navigationGroup): static + { + $this->navigationGroup = $navigationGroup; + + return $this; + } + + public function getNavigationGroup(): string + { + return $this->evaluate($this->navigationGroup) ?? __('filament-env-editor::filament-env-editor.navigation.group'); + } + + public function navigationSort(int|\Closure $navigationSort): static + { + $this->navigationSort = $navigationSort; + + return $this; + } + + public function getNavigationSort(): int + { + return $this->evaluate($this->navigationSort); + } + + public function navigationIcon(string|\Closure $navigationIcon): static + { + $this->navigationIcon = $navigationIcon; + + return $this; + } + + public function getNavigationIcon(): string + { + return $this->evaluate($this->navigationIcon); + } + + public function navigationLabel(string|\Closure|null $navigationLabel): static + { + $this->navigationLabel = $navigationLabel; + + return $this; + } + + public function getNavigationLabel(): string + { + return $this->evaluate($this->navigationLabel) ?? __('filament-env-editor::filament-env-editor.navigation.label'); + } + + public function slug(string|\Closure $slug): static + { + $this->slug = $slug; + + return $this; + } + + public function getSlug(): string + { + return $this->evaluate($this->slug); + } +} diff --git a/src/Pages/Actions/CreateAction.php b/src/Pages/Actions/CreateAction.php new file mode 100644 index 0000000..80c7d7c --- /dev/null +++ b/src/Pages/Actions/CreateAction.php @@ -0,0 +1,80 @@ +label(fn (): string => __('filament-env-editor::filament-env-editor.page.actions.add.title')); + $this->modalHeading(fn ( + ): string => __('filament-env-editor::filament-env-editor.page.actions.add.modalHeading')); + + $this->form([ + TextInput::make('key') + ->label(__('filament-env-editor::filament-env-editor.page.actions.add.form.fields.key')) + ->required(), + TextInput::make('value') + ->label(__('filament-env-editor::filament-env-editor.page.actions.add.form.fields.value')), + Select::make('index') + ->label(__('filament-env-editor::filament-env-editor.page.actions.add.form.fields.index')) + ->helperText(__('filament-env-editor::filament-env-editor.page.actions.add.form.helpText.index')) + ->options(fn () => $this->getExistingKeys()) + ->searchable(), + ]); + + $this->action(function (array $data, ViewEnv $page) { + $result = false; + try { + $options = Arr::get($data, 'index') + ? ['index' => Arr::get($data, 'index')] + : []; + $result = EnvEditor::addKey( + $data['key'], + $data['value'], + $options + ); + $page->refresh(); + $this->successNotificationTitle(fn ( + ): string => __('filament-env-editor::filament-env-editor.page.actions.add.success.title', + ['name' => $data['key']])); + } catch (EnvException $exception) { + $this->failureNotificationTitle($exception->getMessage()); + $this->failure(); + $this->halt(); + } + + $result ? $this->success() : $this->failure(); + }); + + $this->color(Color::Teal); + $this->modalWidth(MaxWidth::FitContent); + } + + /** + * @return array> + */ + private function getExistingKeys(): array + { + return EnvEditor::getEnvFileContent() + ->filter(fn (EntryObj $obj) => !$obj->isSeparator()) + ->groupBy('group') + ->keyBy(fn (Collection $c): string => Str::of($c->first()->key)->before('_')->remove('#')->lower()->prepend('--- ')) + ->map(fn (Collection $c): Collection => $c->mapWithKeys(fn (EntryObj $obj) => [$obj->index => $obj->key])) + ->toArray(); + } +} diff --git a/src/Pages/Actions/DeleteAction.php b/src/Pages/Actions/DeleteAction.php new file mode 100644 index 0000000..1c6e598 --- /dev/null +++ b/src/Pages/Actions/DeleteAction.php @@ -0,0 +1,45 @@ +entry = $obj; + + return $this; + } + + protected function setUp(): void + { + parent::setUp(); + + $this->icon('heroicon-o-trash'); + $this->hiddenLabel(); + $this->color(Color::Rose); + $this->action(function () { + return EnvEditor::deleteKey($this->entry->key); + }); + $this->after(fn (ViewEnv $page) => $page->refresh()); + $this->size(ActionSize::Small); + $this->tooltip(fn (): string => __('filament-env-editor::filament-env-editor.page.actions.delete.tooltip', ['name' => $this->entry->key])); + $this->modalIcon('heroicon-o-trash'); + $this->modalHeading(fn (): string => __('filament-env-editor::filament-env-editor.page.actions.delete.confirm.title', ['name' => $this->entry->key])); + + $this->requiresConfirmation(); + } +} diff --git a/src/Pages/Actions/EditAction.php b/src/Pages/Actions/EditAction.php new file mode 100644 index 0000000..aff7b56 --- /dev/null +++ b/src/Pages/Actions/EditAction.php @@ -0,0 +1,44 @@ +entry = $obj; + + return $this; + } + + protected function setUp(): void + { + parent::setUp(); + + $this->icon('heroicon-c-cog-8-tooth'); + $this->hiddenLabel(); + $this->color(Color::Sky); + $this->form([ + TextInput::make('key')->default(fn () => $this->entry->key)->required(), + TextInput::make('value')->default(fn () => $this->entry->getValue()), + ]); + $this->action(fn (array $data) => EnvEditor::editKey($data['key'], $data['value'])); + $this->size(ActionSize::Small); + $this->modalIcon('heroicon-c-cog-8-tooth'); + $this->modalHeading(__('filament-env-editor::filament-env-editor.page.actions.edit.modal.text')); + $this->tooltip(fn (): string => __('filament-env-editor::filament-env-editor.page.actions.edit.tooltip', ['name' => $this->entry->key])); + } +} diff --git a/src/Pages/Actions/OptimizeAction.php b/src/Pages/Actions/OptimizeAction.php new file mode 100644 index 0000000..19af672 --- /dev/null +++ b/src/Pages/Actions/OptimizeAction.php @@ -0,0 +1,30 @@ +outlined(); + $this->label(fn (): string => __('filament-env-editor::filament-env-editor.page.actions.cache.title')); + $this->tooltip(fn (): string => __('filament-env-editor::filament-env-editor.page.actions.cache.tooltip')); + $this->action(function () { + Artisan::call(OptimizeCommand::class); + + $this->successNotificationTitle(Artisan::output()); + $this->success(); + }); + } +} diff --git a/src/Pages/Actions/OptimizeClearAction.php b/src/Pages/Actions/OptimizeClearAction.php new file mode 100644 index 0000000..cc1d92e --- /dev/null +++ b/src/Pages/Actions/OptimizeClearAction.php @@ -0,0 +1,34 @@ +outlined(); + $this->label(fn (): string => __('filament-env-editor::filament-env-editor.page.actions.clear-cache.title')); + $this->tooltip(fn (): string => __('filament-env-editor::filament-env-editor.page.actions.clear-cache.tooltip')); + $this->action(function () { + Artisan::call(OptimizeClearCommand::class); + + $result = Str::replace('..................................................', '.', Artisan::output()); + + $this->successNotificationTitle(new HtmlString("
$result"));
+            $this->success();
+        });
+    }
+}
diff --git a/src/Pages/ViewEnv.php b/src/Pages/ViewEnv.php
new file mode 100644
index 0000000..c5b54d4
--- /dev/null
+++ b/src/Pages/ViewEnv.php
@@ -0,0 +1,106 @@
+filter(fn (EntryObj $obj) => !$obj->isSeparator())
+            ->groupBy('group')
+            ->map(function (Collection $group) {
+                $fields = $group->map(function (EntryObj $obj) {
+                    return Forms\Components\Group::make([
+                        Forms\Components\Actions::make([
+                            EditAction::make("edit_{$obj->key}")->setEntry($obj),
+                            DeleteAction::make("delete_{$obj->key}")->setEntry($obj),
+                        ])->alignEnd(),
+                        Forms\Components\Placeholder::make($obj->key)
+                            ->label('')
+                            ->content(new HtmlString("{$obj->getAsEnvLine()}"))
+                        ->columnSpan(4),
+                    ])->columns(5);
+                });
+
+                return Forms\Components\Section::make()->schema($fields->all())->columns(1);
+            })->all();
+
+        return $form
+            ->schema($envData);
+    }
+
+    public function refresh(): void
+    {
+        $this->dispatch('envContentUpdated');
+    }
+
+    public static function getNavigationGroup(): ?string
+    {
+        return EnvEditorPlugin::get()->getNavigationGroup();
+    }
+
+    public static function getNavigationSort(): ?int
+    {
+        return EnvEditorPlugin::get()->getNavigationSort();
+    }
+
+    public static function getNavigationIcon(): string
+    {
+        return EnvEditorPlugin::get()->getNavigationIcon();
+    }
+
+    public static function getNavigationLabel(): string
+    {
+        return EnvEditorPlugin::get()->getNavigationLabel();
+    }
+
+    public static function getSlug(): string
+    {
+        return EnvEditorPlugin::get()->getSlug();
+    }
+
+    public function getTitle(): string
+    {
+        return __('filament-env-editor::filament-env-editor.page.title');
+    }
+
+    public static function canAccess(): bool
+    {
+        return EnvEditorPlugin::get()->isAuthorized();
+    }
+}
diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php
new file mode 100644
index 0000000..8341eb0
--- /dev/null
+++ b/src/ServiceProvider.php
@@ -0,0 +1,20 @@
+name('filament-env-editor')
+            ->hasInstallCommand(function (InstallCommand $command) {
+                $command->askToStarRepoOnGitHub('geo-sot/filament-env-editor');
+            })
+            ->hasTranslations()
+            ->hasViews();
+    }
+}