├── .editorconfig ├── .gitattributes ├── .gitignore ├── .php_cs.dist ├── LICENSE ├── README.md ├── app ├── Commands │ ├── .gitkeep │ ├── Command.php │ ├── EnvFreshCommand.php │ ├── EnvPushCommand.php │ ├── EnvSetCommand.php │ ├── EnvSyncCommand.php │ ├── EnvUnsetCommand.php │ ├── InitCommand.php │ ├── OrgCurrentCommand.php │ ├── OrgSwitchCommand.php │ ├── RepoCurrentCommand.php │ ├── RepoSwitchCommand.php │ └── VaultCommand.php ├── Concerns │ ├── AsksForHost.php │ ├── AsksForOrganization.php │ └── AsksForRepository.php ├── Hosts │ ├── Concerns │ │ ├── DecryptsValues.php │ │ ├── EncryptsValues.php │ │ └── SecuresFileContents.php │ ├── Contracts │ │ ├── DriverContract.php │ │ ├── SecurityContract.php │ │ └── WithMapping.php │ ├── Data │ │ ├── File.php │ │ ├── Organization.php │ │ ├── Repository.php │ │ └── User.php │ ├── Drivers │ │ ├── BitbucketDriver.php │ │ ├── Driver.php │ │ ├── FakeDriver.php │ │ ├── GithubDriver.php │ │ └── GitlabDriver.php │ ├── HostManager.php │ └── OrganizationCollection.php ├── Providers │ ├── AppServiceProvider.php │ └── HostServiceProvider.php └── Support │ ├── Helpers.php │ ├── KeyChoiceQuestion.php │ └── Vault.php ├── bootstrap └── app.php ├── box.json ├── builds └── eco ├── composer.json ├── composer.lock ├── config ├── app.php ├── commands.php ├── logging.php └── logo.php ├── eco ├── logo.png ├── logo.svg ├── phpunit.xml.dist ├── resources └── fonts │ └── collosal.flf └── tests ├── CreatesApplication.php ├── Feature ├── EnvFreshCommandTest.php ├── EnvPushCommandTest.php ├── EnvSetCommandTest.php ├── EnvSyncCommandTest.php ├── EnvUnsetCommandTest.php ├── InitCommandTest.php ├── OrgCurrentCommandTest.php ├── OrgSwitchCommandTest.php ├── RepoCurrentCommandTest.php └── RepoSwitchCommandTest.php ├── TestCase.php └── Unit └── Support ├── HelpersTest.php └── VaultTest.php /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 4 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [*.yml] 15 | indent_style = space 16 | indent_size = 2 17 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | /.github export-ignore 3 | .styleci.yml export-ignore 4 | .scrutinizer.yml export-ignore 5 | BACKERS.md export-ignore 6 | CONTRIBUTING.md export-ignore 7 | CHANGELOG.md export-ignore 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | /.idea 3 | /.vscode 4 | /.vagrant 5 | .phpunit.result.cache 6 | .DS_Store 7 | .env 8 | tests/Fixtures/.eco/* 9 | tests/Fixtures/.eco 10 | tests/Fixtures/.env 11 | /public/ 12 | -------------------------------------------------------------------------------- /.php_cs.dist: -------------------------------------------------------------------------------- 1 | setRiskyAllowed(true) 8 | ->setRules([ 9 | '@PSR2' => true, 10 | '@Symfony' => true, 11 | '@PHP70Migration' => true, 12 | '@PHP71Migration' => true, 13 | 'no_multiline_whitespace_before_semicolons' => true, 14 | 'array_syntax' => ['syntax' => 'short'], 15 | 'simplified_null_return' => false, 16 | 'strict_comparison' => true, 17 | 'strict_param' => true, 18 | 'yoda_style' => false, 19 | 'ordered_imports' => true, 20 | 'phpdoc_add_missing_param_annotation' => false, 21 | 'phpdoc_separation' => false, 22 | 'phpdoc_align' => ['align' => 'left'], 23 | ]); 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Adam Campbell 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Eco logo](./logo.svg) 2 | 3 | Eco allows you and your team to effortlessly and securely share non-production environment variables, without the overhead of setting up dedicated secrets servers. 4 | 5 | [![Latest Stable Version](https://poser.pugx.org/hotmeteor/eco-cli/v)](//packagist.org/packages/hotmeteor/eco-cli) 6 | 7 | ### What's this for? 8 | 9 | Have you ever... 10 | - Had a local .env file get deleted or corrupted, causing you to lose environment variables? 11 | - Worked on a team with disorganized or superfluous environment variables? 12 | - Wanted an easy way to securely share environment variables with other project maintainers without need to set up 3rd party secrets management? 13 | - Wanted your team to be able to easily pull an up-to-date copy of project config? 14 | 15 | If you answered "yes" to any of these then Eco is for you! 16 | 17 | **Important:** Eco is _not_ a secure mechanism for storing and sharing **production-level** environment variables. It's not. Please don't. 18 | 19 | ## How it works 20 | 21 | Eco is actually pretty simple. It operates using 3 different storage mechanisms: 22 | 23 | 1. **Your project `.env` file.** This is where the values you're actually using live, because your project depends on them. 24 | 2. **Your local "vault".** The vault is a local config file where you can permanently store any environment variable you don't want to lose. This gives you the ability to nuke your `.env` file and then just pull in the keys you want to restore. 25 | 3. **The remote `.eco` file.** When you push keys you want shared by the team, Eco creates an `.eco` file in the root of your repo, directly in the `master` branch. Inside the `.eco` file are your shared keys, all encrypted using [the same strategy used by Github when storing repository secrets](https://docs.github.com/en/rest/reference/actions#create-or-update-a-repository-secret). This file will store unique key:value pairs that your team pushes to it. 26 | 27 | ## Documentation 28 | 29 | ### Installation 30 | 31 | Eco CLI may be installed globally or on a per-project basis using Composer: 32 | 33 | ```shell script 34 | $ composer require hotmeteor/eco-cli 35 | 36 | $ composer global require hotmeteor/eco-cli 37 | ``` 38 | 39 | ### Getting Started 40 | 41 | Once Eco is installed it needs to be set up. Ideally, this is done within the folder for the project you are collaborating on. 42 | 43 | There are different setups depending on what code host your team uses. 44 | 45 | #### Github 46 | 47 | **Github** requires a personal access token. 48 | 49 | 1. Create a [Github Personal Access Token](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token) is required. 50 | - Choose `repo` and `read:org` permissions 51 | 52 | #### Gitlab 53 | 54 | **Gitlab** requires a personal access token and a Deploy Key. 55 | 56 | 1. Create a Personal Access Token: https://gitlab.com/profile/personal_access_tokens 57 | - Choose `api` privileges 58 | 2. Create a Deploy Key for your project: https://gitlab.com/[group]/[project]/-/settings/repository 59 | - Name it `eco-cli` 60 | - Make sure "Write access allowed" is checked 61 | 62 | #### Bitbucket 63 | 64 | **Bitbucket** requires an App Password and an Access Key. 65 | 66 | 1. Create an App Password: https://bitbucket.org/account/settings/app-passwords/ 67 | - Select: 68 | - Account Email, Read 69 | - Project Read 70 | - Repositories Read, Write, Admin 71 | 2. Create an Access Key for your project: https://bitbucket.org/[workspace]/[project]/admin/access-keys/ 72 | - Name it `eco-cli` 73 | 3. When authenticating Eco, your credentials will be your typical Bitbucket username and your new app password. 74 | 75 | ## Usage 76 | 77 | Eco comes with a number of commands to manage local and remote environment variables. 78 | 79 | #### Setup 80 | 81 | ```sh 82 | $ eco init 83 | ``` 84 | The first thing you run after installing. 85 | 86 | You will be asked to select the code host your team uses, as well as provide the proper credentials. You will then be asked to select the owner or organization to act under, as well as the repository for the current project. 87 | 88 | ```sh 89 | $ eco vault 90 | ``` 91 | View all the values in your Vault. 92 | 93 | #### Organizations 94 | 95 | ```sh 96 | $ eco org:switch 97 | ``` 98 | 99 | List available organizations you're a member of and allow you switch to a different one. 100 | 101 | ```sh 102 | $ eco org:current 103 | ``` 104 | 105 | Show the current working organization. 106 | 107 | #### Repositories 108 | 109 | 110 | ```sh 111 | $ eco repo:list 112 | ``` 113 | 114 | List available repositories in your organization. 115 | 116 | ```sh 117 | $ eco repo:switch 118 | ``` 119 | 120 | List available repositories in your organization and allow you switch to a different repo. This allows you to use Eco across different repositories. Just don't forget to switch repos before pushing or pulling! 121 | 122 | ```sh 123 | $ eco repo:current 124 | ``` 125 | 126 | Show the current working repository. 127 | 128 | #### Keys 129 | 130 | ```sh 131 | $ eco env:fresh 132 | ``` 133 | 134 | Fetch the `.env.example` file from your project repository and copy it as your new local `.env` file. This is a desctructive command, so you are asked to confirm. 135 | 136 | ```sh 137 | $ eco env:set 138 | ``` 139 | 140 | Create or update a key:value pair in your local vault and will add it to your local `.env` file. 141 | 142 | ```sh 143 | $ eco env:unset 144 | ``` 145 | 146 | Remove a key:value pair from your local vault and will remove it from your local `.env` file. 147 | 148 | ```sh 149 | $ eco env:push 150 | ``` 151 | 152 | Push a key:value pair into the remote `.eco` file. 153 | 154 | 155 | ```sh 156 | $ eco env:sync 157 | ``` 158 | 159 | Sync all key:value pairs from the remote `.eco` file with your local `.env` file. You will be asked to confirm before overwriting any local values with remote values. 160 | 161 | ## Contributing 162 | 163 | If you're interested in contributing to Eco, please read our [contributing guide](https://github.com/hotmeteor/eco-cli/blob/master/.github/CONTRIBUTING.md). 164 | 165 | #### Acknowledgments 166 | 167 | Built on the fantastic [Laravel Zero](https://laravel-zero.com/) framework by [@nunomaduro](https://github.com/nunomaduro) 168 | 169 | Inspired by the work done on the [Vapor CLI](https://github.com/laravel/vapor-cli), which provided some foundational code for this CLI. 170 | -------------------------------------------------------------------------------- /app/Commands/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hotmeteor/eco-cli/c9b3e81d770f7fdb3f50470c2e518c116ff76b4d/app/Commands/.gitkeep -------------------------------------------------------------------------------- /app/Commands/Command.php: -------------------------------------------------------------------------------- 1 | host = app(HostManager::class); 41 | 42 | $this->setAliases($this->aliases); 43 | 44 | if (!Vault::get('version')) { 45 | Vault::set('version', app('git.version')); 46 | } 47 | } 48 | 49 | protected function driver() 50 | { 51 | return $this->host->driver(Vault::get('driver')); 52 | } 53 | 54 | public function envFile() 55 | { 56 | return config('app.env_path').'/'.$this->env_file; 57 | } 58 | 59 | public function vaultFile() 60 | { 61 | return $this->vault_file; 62 | } 63 | 64 | /** 65 | * Ensure that the user has authenticated with Eco. 66 | * 67 | * @return void 68 | */ 69 | public function authenticate() 70 | { 71 | try { 72 | $credentials = $this->getCredentials(); 73 | call_user_func_array([$this->driver(), 'authenticate'], $credentials); 74 | $this->current_user = $this->currentUser(); 75 | } catch (InvalidArgumentException $exception) { 76 | $this->resetCredentials(); 77 | 78 | $this->abort($exception->getMessage()); 79 | } catch (\Exception $exception) { 80 | $this->resetCredentials(); 81 | 82 | $this->abort( 83 | $exception->getMessage() === 'Bad credentials' 84 | ? 'Invalid token.' 85 | : $exception->getMessage() 86 | ); 87 | } 88 | } 89 | 90 | protected function currentUser() 91 | { 92 | return $this->current_user ?? $this->driver()->getCurrentUser(); 93 | } 94 | 95 | protected function resetCredentials() 96 | { 97 | Vault::config('token', ''); 98 | Vault::config('username', ''); 99 | Vault::config('password', ''); 100 | } 101 | 102 | protected function getCredentials(): array 103 | { 104 | return ($driver = $this->askForHost()) === 'bitbucket' 105 | ? $this->askForUsernamePassword() 106 | : $this->askForToken(); 107 | } 108 | 109 | protected function askForHost() 110 | { 111 | if (!empty(Vault::get('driver'))) { 112 | return Vault::get('driver'); 113 | } 114 | 115 | Vault::set('driver', $driver = $this->asksForHost()); 116 | 117 | return $driver; 118 | } 119 | 120 | protected function askForToken() 121 | { 122 | $token = Vault::config('token'); 123 | 124 | if (empty($token)) { 125 | Vault::config('token', $token = $this->secret('Token')); 126 | } 127 | 128 | return [$token, null]; 129 | } 130 | 131 | protected function askForUsernamePassword() 132 | { 133 | $username = Vault::config('username'); 134 | $password = Vault::config('password'); 135 | 136 | if (empty($username) || empty($password)) { 137 | Vault::config('username', $username = $this->ask('Username', $username)); 138 | Vault::config('password', $password = $this->secret('Password')); 139 | } 140 | 141 | return [$username, $password]; 142 | } 143 | 144 | public function abort($text) 145 | { 146 | $this->danger($text); 147 | 148 | exit(1); 149 | } 150 | 151 | public function danger($text) 152 | { 153 | $this->output->writeln(''.$text.''); 154 | } 155 | 156 | public function keyChoice($title, $choices) 157 | { 158 | return $this->output->askQuestion( 159 | new KeyChoiceQuestion($title, $choices) 160 | ); 161 | } 162 | 163 | protected function findLine($file, $key) 164 | { 165 | return Env::has($file, $key); 166 | } 167 | 168 | protected function setLine($file, $key, $value) 169 | { 170 | Env::set($file, $key, $value); 171 | } 172 | 173 | protected function unsetLine($file, $key) 174 | { 175 | Env::unset($file, $key); 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /app/Commands/EnvFreshCommand.php: -------------------------------------------------------------------------------- 1 | confirm('Are you sure you want a fresh .env? This will overwrite your existing .env file.')) { 31 | $this->authenticate(); 32 | 33 | $file = $this->driver()->getRemoteFile( 34 | Vault::config('org'), Vault::config('repo'), $this->env_example_file 35 | ); 36 | 37 | if (!$file) { 38 | $this->abort("Unable to find {$this->env_example_file} file in repo."); 39 | } 40 | 41 | file_put_contents($this->envFile(), $file->contents); 42 | 43 | $this->output->writeln('Your .env file has been refreshed.'); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/Commands/EnvPushCommand.php: -------------------------------------------------------------------------------- 1 | authenticate(); 38 | 39 | $owner = Vault::config('org'); 40 | $repo = Vault::config('repo'); 41 | 42 | if (!$repo) { 43 | $this->abort('You must have an organization and repository selected.'); 44 | } 45 | 46 | $key = $this->asksForKey(); 47 | 48 | $value = $this->ask('What is the value?'); 49 | 50 | if ($this->keyExists($owner, $repo, $key)) { 51 | if ($this->confirm('This environment key already exists. Are you sure you want to change it?')) { 52 | $this->setValue($owner, $repo, $key, $value); 53 | } 54 | } else { 55 | $this->setValue($owner, $repo, $key, $value); 56 | } 57 | } 58 | 59 | protected function asksForKey() 60 | { 61 | if (empty($key = $this->argument('key'))) { 62 | $key = $this->ask('What key should be pushed?'); 63 | } 64 | 65 | return strtoupper(trim($key)); 66 | } 67 | 68 | protected function keyExists($owner, $repo, $key): bool 69 | { 70 | $this->setSecretKey($owner, $repo); 71 | 72 | try { 73 | $file = $this->driver()->getRemoteFile( 74 | $owner, $repo, $this->vaultFile() 75 | ); 76 | 77 | $decrypted = $this->driver()->decryptContents( 78 | $file->contents, $this->secret_key 79 | ); 80 | 81 | $this->hash = $file->hash; 82 | $this->values = $decrypted; 83 | 84 | return array_key_exists($key, $decrypted); 85 | } catch (\Exception $exception) { 86 | $this->values = []; 87 | 88 | return false; 89 | } 90 | } 91 | 92 | protected function setSecretKey($owner, $repo): void 93 | { 94 | $this->secret_key = $this->driver()->getSecretKey($owner, $repo); 95 | } 96 | 97 | protected function setValue($owner, $repo, $key, $value): void 98 | { 99 | $payload = $this->driver()->encryptContents( 100 | Arr::set($this->values, $key, $value), 101 | $this->secret_key 102 | ); 103 | 104 | try { 105 | if ($this->hash) { 106 | $this->driver()->updateRemoteFile( 107 | $owner, $repo, $this->vaultFile(), $payload, 'Update .eco values', $this->hash 108 | ); 109 | } else { 110 | $this->driver()->createRemoteFile( 111 | $owner, $repo, $this->vaultFile(), $payload, 'Create initial .eco file' 112 | ); 113 | } 114 | 115 | if ($this->option('set')) { 116 | Vault::set("{$owner}.{$repo}.{$key}", $value); 117 | } 118 | 119 | $this->info('The value was successfully added to the .eco file.'); 120 | } catch (\Exception $exception) { 121 | $this->abort('There was an issue updating the .eco file.'); 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /app/Commands/EnvSetCommand.php: -------------------------------------------------------------------------------- 1 | asksForKey(); 33 | 34 | if (str_contains($key, '=')) { 35 | [$key, $value] = explode('=', $key); 36 | } else { 37 | $value = $this->ask('What is the value?'); 38 | } 39 | 40 | $org = Vault::config('org'); 41 | $repo = Vault::config('repo'); 42 | 43 | $key = Helpers::formatKey($key); 44 | $value = Helpers::formatValue($value); 45 | 46 | Vault::set("{$org}.{$repo}.{$key}", $value); 47 | 48 | $this->setLine($this->envFile(), $key, $value); 49 | 50 | $this->output->writeln('The '.$key.' value has been stored and added to your .env file.'); 51 | } 52 | 53 | protected function asksForKey() 54 | { 55 | if (empty($key = $this->argument('key'))) { 56 | $key = $this->ask('What key should be set?'); 57 | } 58 | 59 | return $key; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/Commands/EnvSyncCommand.php: -------------------------------------------------------------------------------- 1 | authenticate(); 34 | 35 | $this->info('Syncing will use your local variables, but ask you about conflicting remote variables.'); 36 | 37 | $org = Vault::config('org'); 38 | $repo = Vault::config('repo'); 39 | 40 | $this->task('Setting up file', function () { 41 | return $this->setupFile(); 42 | }); 43 | 44 | $this->task('Assigning local values', function () use ($org, $repo) { 45 | return $this->assignLocalValues($org, $repo); 46 | }); 47 | 48 | $this->task('Assigning remote values', function () use ($org, $repo) { 49 | return $this->assignRemoteValues($org, $repo); 50 | }); 51 | 52 | $this->line('Your .env file has been synced.'); 53 | } 54 | 55 | protected function setupFile() 56 | { 57 | if (!file_exists($this->envFile())) { 58 | file_put_contents($this->envFile(), '', true); 59 | } 60 | 61 | return true; 62 | } 63 | 64 | protected function assignLocalValues($org, $repo): bool 65 | { 66 | $data = Vault::get("{$org}.{$repo}") ?? []; 67 | 68 | foreach ($data as $key => $value) { 69 | $this->setLine($this->envFile(), $key, $value); 70 | } 71 | 72 | return true; 73 | } 74 | 75 | protected function assignRemoteValues($owner, $repo) 76 | { 77 | try { 78 | $file = $this->driver()->getRemoteFile( 79 | $owner, $repo, $this->vaultFile() 80 | ); 81 | 82 | $data = $this->driver()->decryptContents( 83 | $file->contents, $this->driver()->getSecretKey($owner, $repo) 84 | ); 85 | 86 | foreach ($data as $key => $value) { 87 | if (!$this->option('force') && $this->findLine($this->envFile(), $key)) { 88 | if ($this->confirm("The {$key} variable already exists in your local .env. Do you want to overwrite it?")) { 89 | $this->set($owner, $repo, $key, $value); 90 | } 91 | } else { 92 | $this->set($owner, $repo, $key, $value); 93 | } 94 | } 95 | 96 | return true; 97 | } catch (\Exception $exception) { 98 | $this->danger('There was an issue with the remote file.'); 99 | } 100 | } 101 | 102 | protected function set($owner, $repo, $key, $value) 103 | { 104 | $key = Helpers::formatKey($key); 105 | $value = Helpers::formatValue($value); 106 | 107 | $this->setLine($this->envFile(), $key, $value); 108 | 109 | if ($this->option('store')) { 110 | Vault::set("{$owner}.{$repo}.{$key}", $value); 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /app/Commands/EnvUnsetCommand.php: -------------------------------------------------------------------------------- 1 | asksForKey(); 33 | 34 | $org = Vault::config('org'); 35 | $repo = Vault::config('repo'); 36 | 37 | $key = Helpers::formatKey($key); 38 | 39 | Vault::unset("{$org}.{$repo}.{$key}"); 40 | 41 | $this->unsetLine($this->envFile(), $key); 42 | 43 | $this->output->writeln('The '.$key.' value has been deleted and removed from your .env file.'); 44 | } 45 | 46 | protected function asksForKey() 47 | { 48 | if (empty($key = $this->argument('key'))) { 49 | $key = $this->ask('What key should be unset?'); 50 | } 51 | 52 | return strtoupper(trim($key)); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/Commands/InitCommand.php: -------------------------------------------------------------------------------- 1 | Github: https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token 32 | Gitlab: https://gitlab.com/profile/personal_access_tokens 33 | Bitbucket: https://bitbucket.org/account/settings/app-passwords/ 34 | '; 35 | 36 | /** 37 | * Execute the console command. 38 | * 39 | * @return mixed 40 | */ 41 | public function handle() 42 | { 43 | try { 44 | $this->attemptLogin(); 45 | } catch (ClientException $e) { 46 | return $this->displayFailureMessage($e->getResponse()); 47 | } 48 | 49 | $this->ensureCurrentOrgIsSet(); 50 | $this->ensureCurrentRepoIsSet(); 51 | 52 | $this->info(Helpers::exclaim().'! Eco has been configured.'); 53 | } 54 | 55 | /** 56 | * Attempt to log in. 57 | * 58 | * @return string 59 | */ 60 | protected function attemptLogin() 61 | { 62 | $this->info('----'); 63 | // $this->info('To start, you will need a Github Personal Access token.'); 64 | // $this->comment('https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token'); 65 | 66 | Vault::unset('driver'); 67 | 68 | $this->authenticate(); 69 | 70 | $this->info('Authenticated successfully.'.PHP_EOL); 71 | } 72 | 73 | protected function displayFailureMessage($response) 74 | { 75 | $this->abort('Authentication failed ('.$response->getStatusCode().')'); 76 | } 77 | 78 | protected function ensureCurrentOrgIsSet() 79 | { 80 | Vault::config('org', $this->asksForOrganization()); 81 | 82 | $this->info('Organization set successfully.'); 83 | } 84 | 85 | protected function ensureCurrentRepoIsSet() 86 | { 87 | Vault::config('repo', $this->asksForRepository()); 88 | 89 | $this->info('Repository set successfully.'); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /app/Commands/OrgCurrentCommand.php: -------------------------------------------------------------------------------- 1 | abort('Unable to determine current organization.'); 32 | } 33 | 34 | $this->output->writeln('You are currently working in the ['.$org.'] organization.'); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/Commands/OrgSwitchCommand.php: -------------------------------------------------------------------------------- 1 | authenticate(); 34 | 35 | Vault::config('org', $this->asksForOrganization()); 36 | Vault::config('repo', ''); 37 | 38 | $this->info('Organization set successfully.'); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/Commands/RepoCurrentCommand.php: -------------------------------------------------------------------------------- 1 | abort('Unable to determine current repo.'); 32 | } 33 | 34 | $this->output->writeln('You are currently working in the ['.$repo.'] repository.'); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/Commands/RepoSwitchCommand.php: -------------------------------------------------------------------------------- 1 | option('name'))) { 35 | $this->authenticate(); 36 | 37 | $name = $this->asksForRepository(); 38 | } else { 39 | $name = $this->option('name'); 40 | } 41 | 42 | Vault::config('repo', $name); 43 | 44 | $this->info('Repository set successfully.'); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/Commands/VaultCommand.php: -------------------------------------------------------------------------------- 1 | keyChoice('What code host do you use?', [ 10 | 'github' => 'Github', 11 | 'gitlab' => 'Gitlab', 12 | 'bitbucket' => 'Bitbucket', 13 | ]); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/Concerns/AsksForOrganization.php: -------------------------------------------------------------------------------- 1 | driver()->getOrganizations()->sortBy->login; 13 | 14 | if (Vault::get('driver') !== 'bitbucket') { 15 | $organizations->prepend($this->currentUser()); 16 | } 17 | 18 | return $organizations->firstWhere('id', $this->getOrganizationChoice($organizations))->login; 19 | } 20 | 21 | protected function getOrganizationChoice(Collection $organizations) 22 | { 23 | return $this->keyChoice( 24 | 'Which organization should be used?', 25 | $organizations->mapWithKeys(function ($org) { 26 | return [$org->id => $org->login]; 27 | })->all() 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/Concerns/AsksForRepository.php: -------------------------------------------------------------------------------- 1 | currentUser()->login 15 | ? $this->driver()->getCurrentUserRepositories() 16 | : $this->driver()->getOwnerRepositories($org); 17 | 18 | $id = $this->getRepositoryChoice($repos); 19 | 20 | return is_numeric($id) ? $repos->firstWhere('id', $id)->name : $id; 21 | } 22 | 23 | protected function getRepositoryChoice(Collection $repositories) 24 | { 25 | return $this->choice( 26 | 'Which repository should be used? You can always switch this later.', 27 | $repositories->sortBy->name->mapWithKeys(function ($repo) { 28 | return [$repo->id => $repo->name]; 29 | })->all() 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/Hosts/Concerns/DecryptsValues.php: -------------------------------------------------------------------------------- 1 | values, true); 15 | $nonce = base64_decode($data->nonce, true); 16 | 17 | return json_decode(self::decrypt($public_key, $values, $nonce), true); 18 | } 19 | 20 | public function encryptContents(array $values, $public_key) 21 | { 22 | $nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES); 23 | 24 | return json_encode([ 25 | 'values' => base64_encode(self::encrypt($public_key, json_encode($values), $nonce)), 26 | 'nonce' => base64_encode($nonce), 27 | ]); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/Hosts/Contracts/DriverContract.php: -------------------------------------------------------------------------------- 1 | contents = $contents; 14 | 15 | $this->hash = $hash; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/Hosts/Data/Organization.php: -------------------------------------------------------------------------------- 1 | id = $id; 14 | $this->name = $name; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/Hosts/Data/User.php: -------------------------------------------------------------------------------- 1 | id = $id; 14 | $this->login = $login; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/Hosts/Drivers/BitbucketDriver.php: -------------------------------------------------------------------------------- 1 | client; 20 | } 21 | 22 | public function authenticate($tokenOrUsername, $password = null) 23 | { 24 | $this->client()->authenticate(Client::AUTH_HTTP_PASSWORD, $tokenOrUsername, $password); 25 | } 26 | 27 | public function getCurrentUser() 28 | { 29 | $user = $this->client()->currentUser()->show(); 30 | 31 | return new User($user['uuid'], $user['username']); 32 | } 33 | 34 | public function getOrganizations() 35 | { 36 | return $this->collectOrganizations( 37 | Arr::get($this->client()->currentUser()->listWorkspaces(), 'values', []) 38 | ); 39 | } 40 | 41 | public function getCurrentUserRepositories($per_page = 100) 42 | { 43 | return $this->getOwnerRepositories(Vault::config('username')); 44 | } 45 | 46 | public function getOwnerRepositories($owner, $per_page = 100) 47 | { 48 | return $this->collectRepositories( 49 | Arr::get($this->client()->repositories()->workspaces($owner)->list(), 'values', []) 50 | ); 51 | } 52 | 53 | public function getRepository($owner, $name) 54 | { 55 | return $this->client()->repositories()->workspaces($owner)->show($name); 56 | } 57 | 58 | public function getSecretKey($owner, $repository) 59 | { 60 | $keys = collect($this->client()->repositories()->workspaces($owner)->deployKeys($repository)->list()['values']); 61 | 62 | if ($keys->contains('label', 'eco-cli')) { 63 | $key = $keys->firstWhere('label', 'eco-cli')['key']; 64 | $key = trim(Str::after($key, 'ssh-rsa')); 65 | $key = substr($key, 0, SODIUM_CRYPTO_SECRETBOX_KEYBYTES); 66 | 67 | return base64_encode($key); 68 | } 69 | } 70 | 71 | public function getRemoteFile($owner, $repository, $filename): File 72 | { 73 | $file = $this->client()->repositories()->workspaces($owner)->src($repository)->download('master', $filename); 74 | 75 | $contents = $file->getContents(); 76 | 77 | return new File($contents, base64_encode($contents)); 78 | } 79 | 80 | public function createRemoteFile($owner, $repository, $file, $contents, $message) 81 | { 82 | return $this->updateRemoteFile($owner, $repository, $file, $contents, $message); 83 | } 84 | 85 | public function updateRemoteFile($owner, $repository, $file, $contents, $message, $sha = null) 86 | { 87 | return $this->client()->repositories()->workspaces($owner)->src($repository)->createWithFiles( 88 | [new FileResource($file, $contents)], 89 | ['message' => $message] 90 | ); 91 | } 92 | 93 | public function mapOrganization($item): Organization 94 | { 95 | return new Organization($item['slug'], $item['slug']); 96 | } 97 | 98 | public function mapRepository($item): Repository 99 | { 100 | return new Repository($item['name'], $item['name']); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /app/Hosts/Drivers/Driver.php: -------------------------------------------------------------------------------- 1 | client = $client; 19 | } 20 | 21 | abstract protected function client(); 22 | 23 | protected function collectOrganizations(array $items) 24 | { 25 | return collect($items)->map(function ($item) { 26 | return static::mapOrganization($item); 27 | }); 28 | } 29 | 30 | protected function collectRepositories(array $items) 31 | { 32 | return collect($items)->map(function ($item) { 33 | return static::mapRepository($item); 34 | }); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/Hosts/Drivers/FakeDriver.php: -------------------------------------------------------------------------------- 1 | client; 18 | } 19 | 20 | public function authenticate($tokenOrUsername, $password = null) 21 | { 22 | $this->client()->authenticate($tokenOrUsername, null, Client::AUTH_ACCESS_TOKEN); 23 | } 24 | 25 | public function getCurrentUser(): User 26 | { 27 | $user = $this->client()->currentUser()->show(); 28 | 29 | return new User($user['id'], $user['login']); 30 | } 31 | 32 | public function getOrganizations(): Collection 33 | { 34 | return $this->collectOrganizations( 35 | $this->client()->currentUser()->organizations() 36 | ); 37 | } 38 | 39 | public function getOwnerRepositories($owner, $per_page = 100): Collection 40 | { 41 | return $this->collectRepositories( 42 | $this->client()->api('organization')->setPerPage($per_page)->repositories($owner) 43 | ); 44 | } 45 | 46 | public function getCurrentUserRepositories($per_page = 100): Collection 47 | { 48 | return $this->collectRepositories( 49 | $this->client()->currentUser()->setPerPage($per_page)->repositories() 50 | ); 51 | } 52 | 53 | public function getRepository($owner, $name) 54 | { 55 | return $this->client()->repository()->show($owner, $name); 56 | } 57 | 58 | public function getSecretKey($owner, $repository) 59 | { 60 | $response = $this->client()->getHttpClient()->get("/repos/{$owner}/{$repository}/actions/secrets/public-key"); 61 | 62 | $content = ResponseMediator::getContent($response); 63 | 64 | return $content['key']; 65 | } 66 | 67 | public function getRemoteFile($owner, $repository, $filename): File 68 | { 69 | $response = $this->client()->api('repositories')->contents()->show( 70 | $owner, $repository, $filename 71 | ); 72 | 73 | return new File( 74 | base64_decode($response['content'], true), 75 | $response['sha'] 76 | ); 77 | } 78 | 79 | public function createRemoteFile($owner, $repository, $file, $contents, $message) 80 | { 81 | return $this->client()->api('repositories')->contents()->create( 82 | $owner, $repository, $file, $contents, $message 83 | ); 84 | } 85 | 86 | public function updateRemoteFile($owner, $repository, $file, $contents, $message, $sha = null) 87 | { 88 | return $this->client()->api('repositories')->contents()->update( 89 | $owner, $repository, $file, $contents, $message, $sha 90 | ); 91 | } 92 | 93 | public function mapOrganization($item): Organization 94 | { 95 | return new Organization($item['id'], $item['login']); 96 | } 97 | 98 | public function mapRepository($item): Repository 99 | { 100 | return new Repository($item['id'], $item['name']); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /app/Hosts/Drivers/GitlabDriver.php: -------------------------------------------------------------------------------- 1 | client; 18 | } 19 | 20 | public function authenticate($tokenOrUsername, $password = null) 21 | { 22 | // $this->driver()->setUrl('https://git.yourdomain.com'); 23 | 24 | $this->client()->authenticate($tokenOrUsername, Client::AUTH_HTTP_TOKEN); 25 | } 26 | 27 | public function getCurrentUser(): User 28 | { 29 | $user = $this->client()->users()->me(); 30 | 31 | return new User($user['id'], $user['username']); 32 | } 33 | 34 | public function getOrganizations(): Collection 35 | { 36 | return $this->collectOrganizations( 37 | $this->client()->groups()->all() 38 | ); 39 | } 40 | 41 | public function getCurrentUserRepositories($per_page = 100): Collection 42 | { 43 | return $this->collectRepositories( 44 | $this->client()->projects()->all([ 45 | 'membership' => true, 46 | 'owned' => true, 47 | 'simple' => true, 48 | ]) 49 | ); 50 | } 51 | 52 | public function getOwnerRepositories($owner, $per_page = 100): Collection 53 | { 54 | return $this->collectRepositories( 55 | $this->client()->projects()->all([ 56 | 'membership' => true, 57 | 'simple' => true, 58 | ]) 59 | ); 60 | } 61 | 62 | public function getRepository($owner, $name) 63 | { 64 | return $this->client()->projects()->show("$owner/$name"); 65 | } 66 | 67 | public function getSecretKey($owner, $repository) 68 | { 69 | $keys = collect($this->client()->projects()->deployKeys("$owner/$repository")); 70 | 71 | if ($keys->isNotEmpty()) { 72 | $key = $keys->first()['key']; 73 | $key = trim(Str::between($key, 'ssh-ed25519', 'Gitlab')); 74 | $key = substr($key, 0, SODIUM_CRYPTO_SECRETBOX_KEYBYTES); 75 | 76 | return base64_encode($key); 77 | } 78 | 79 | return null; 80 | } 81 | 82 | public function getRemoteFile($owner, $repository, $filename): File 83 | { 84 | $response = $this->client()->repositoryFiles()->getFile( 85 | "$owner/$repository", $filename, 'master' 86 | ); 87 | 88 | return new File( 89 | base64_decode($response['content'], true), 90 | $response['commit_id'] 91 | ); 92 | } 93 | 94 | public function createRemoteFile($owner, $repository, $file, $contents, $message) 95 | { 96 | return $this->client()->repositoryFiles()->createFile( 97 | "$owner/$repository", [ 98 | 'branch' => 'master', 99 | 'file_path' => urlencode($file), 100 | 'content' => $contents, 101 | 'commit_message' => $message, 102 | ] 103 | ); 104 | } 105 | 106 | public function updateRemoteFile($owner, $repository, $file, $contents, $message, $sha = null) 107 | { 108 | return $this->client()->repositoryFiles()->updateFile( 109 | "$owner/$repository", [ 110 | 'branch' => 'master', 111 | 'file_path' => urlencode($file), 112 | 'content' => $contents, 113 | 'commit_message' => $message, 114 | ] 115 | ); 116 | } 117 | 118 | public function mapOrganization($item): Organization 119 | { 120 | return new Organization($item['id'], $item['name']); 121 | } 122 | 123 | public function mapRepository($item): Repository 124 | { 125 | return new Repository($item['id'], $item['name']); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /app/Hosts/HostManager.php: -------------------------------------------------------------------------------- 1 | app->singleton(HostManager::class, function ($app) { 15 | return new HostManager($app); 16 | }); 17 | 18 | $this->app->alias( 19 | HostManager::class, DriverContract::class 20 | ); 21 | 22 | $this->app->alias( 23 | HostManager::class, SecurityContract::class 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/Support/Helpers.php: -------------------------------------------------------------------------------- 1 | singleton( 30 | Illuminate\Contracts\Console\Kernel::class, 31 | LaravelZero\Framework\Kernel::class 32 | ); 33 | 34 | $app->singleton( 35 | Illuminate\Contracts\Debug\ExceptionHandler::class, 36 | Illuminate\Foundation\Exceptions\Handler::class 37 | ); 38 | 39 | /* 40 | |-------------------------------------------------------------------------- 41 | | Return The Application 42 | |-------------------------------------------------------------------------- 43 | | 44 | | This script returns the application instance. The instance is given to 45 | | the calling script so we can separate the building of the instances 46 | | from the actual running of the application and sending responses. 47 | | 48 | */ 49 | 50 | return $app; 51 | -------------------------------------------------------------------------------- /box.json: -------------------------------------------------------------------------------- 1 | { 2 | "chmod": "0755", 3 | "directories": [ 4 | "app", 5 | "bootstrap", 6 | "config", 7 | "vendor" 8 | ], 9 | "files": [ 10 | "composer.json", 11 | "resources/fonts/collosal.flf" 12 | ], 13 | "exclude-composer-files": false, 14 | "compression": "GZ", 15 | "compactors": [ 16 | "KevinGH\\Box\\Compactor\\Php", 17 | "KevinGH\\Box\\Compactor\\Json" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /builds/eco: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hotmeteor/eco-cli/c9b3e81d770f7fdb3f50470c2e518c116ff76b4d/builds/eco -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hotmeteor/eco-cli", 3 | "description": "Eco CLI for Laravel .env syncing", 4 | "keywords": ["console", "cli", "env", "environment", "eco"], 5 | "type": "project", 6 | "license": "MIT", 7 | "support": { 8 | "issues": "https://github.com/hotmeteor/eco-cli/issues", 9 | "source": "https://github.com/hotmeteor/eco-cli" 10 | }, 11 | "authors": [ 12 | { 13 | "name": "Adam Campbell", 14 | "email": "adam@hotmeteor.com" 15 | } 16 | ], 17 | "require": { 18 | "php": "^7.4|^8.0", 19 | "ext-json": "*", 20 | "ext-sodium": "*", 21 | "bitbucket/client": "^3.1", 22 | "guzzlehttp/guzzle": "^7.0.1", 23 | "hotmeteor/eco-env": "^1.1", 24 | "http-interop/http-factory-guzzle": "^1.0", 25 | "illuminate/container": "^8.0", 26 | "illuminate/filesystem": "^8.0", 27 | "illuminate/log": "^8.0", 28 | "illuminate/support": "^8.0", 29 | "knplabs/github-api": "^3.0", 30 | "laminas/laminas-text": "^2.7", 31 | "laravel-zero/framework": "^8.0", 32 | "m4tthumphrey/php-gitlab-api": "^10.0", 33 | "padraic/phar-updater": "^1.0.6", 34 | "ramsey/uuid": "^3.7|^4.0", 35 | "symfony/console": "^4.2|^5.0", 36 | "symfony/process": "^4.2|^5.0", 37 | "symfony/var-dumper": "^4.2|^5.0", 38 | "symfony/yaml": "^4.2|^5.0" 39 | }, 40 | "require-dev": { 41 | "mockery/mockery": "^1.4.2", 42 | "phpunit/phpunit": "^9.3" 43 | }, 44 | "autoload": { 45 | "psr-4": { 46 | "App\\": "app/" 47 | } 48 | }, 49 | "autoload-dev": { 50 | "psr-4": { 51 | "Tests\\": "tests/" 52 | } 53 | }, 54 | "config": { 55 | "preferred-install": "dist", 56 | "sort-packages": true, 57 | "optimize-autoloader": true 58 | }, 59 | "minimum-stability": "dev", 60 | "prefer-stable": true, 61 | "bin": ["builds/eco"] 62 | } 63 | -------------------------------------------------------------------------------- /config/app.php: -------------------------------------------------------------------------------- 1 | 'Eco CLI', 16 | 17 | /* 18 | |-------------------------------------------------------------------------- 19 | | Application Version 20 | |-------------------------------------------------------------------------- 21 | | 22 | | This value determines the "version" your application is currently running 23 | | in. You may want to follow the "Semantic Versioning" - Given a version 24 | | number MAJOR.MINOR.PATCH when an update happens: https://semver.org. 25 | | 26 | */ 27 | 28 | 'version' => app('git.version'), 29 | 30 | /* 31 | |-------------------------------------------------------------------------- 32 | | Application Environment 33 | |-------------------------------------------------------------------------- 34 | | 35 | | This value determines the "environment" your application is currently 36 | | running in. This may determine how you prefer to configure various 37 | | services the application utilizes. This can be overridden using 38 | | the global command line "--env" option when calling commands. 39 | | 40 | */ 41 | 42 | 'env' => 'development', 43 | 44 | /* 45 | |-------------------------------------------------------------------------- 46 | | Autoloaded Service Providers 47 | |-------------------------------------------------------------------------- 48 | | 49 | | The service providers listed here will be automatically loaded on the 50 | | request to your application. Feel free to add your own services to 51 | | this array to grant expanded functionality to your applications. 52 | | 53 | */ 54 | 55 | 'providers' => [ 56 | App\Providers\AppServiceProvider::class, 57 | App\Providers\HostServiceProvider::class, 58 | ], 59 | 60 | /* 61 | |-------------------------------------------------------------------------- 62 | | Vault location 63 | |-------------------------------------------------------------------------- 64 | | 65 | | The default location for the local .eco vault file. 66 | | 67 | */ 68 | 69 | 'home_path' => $_SERVER['HOME'] ?? $_SERVER['USERPROFILE'], 70 | 71 | /* 72 | |-------------------------------------------------------------------------- 73 | | .env location 74 | |-------------------------------------------------------------------------- 75 | | 76 | | The default location to search for .env files. 77 | | 78 | */ 79 | 80 | 'env_path' => base_path('/'), 81 | ]; 82 | -------------------------------------------------------------------------------- /config/commands.php: -------------------------------------------------------------------------------- 1 | NunoMaduro\LaravelConsoleSummary\SummaryCommand::class, 16 | 17 | /* 18 | |-------------------------------------------------------------------------- 19 | | Commands Paths 20 | |-------------------------------------------------------------------------- 21 | | 22 | | This value determines the "paths" that should be loaded by the console's 23 | | kernel. Foreach "path" present on the array provided below the kernel 24 | | will extract all "Illuminate\Console\Command" based class commands. 25 | | 26 | */ 27 | 28 | 'paths' => [app_path('Commands')], 29 | 30 | /* 31 | |-------------------------------------------------------------------------- 32 | | Added Commands 33 | |-------------------------------------------------------------------------- 34 | | 35 | | You may want to include a single command class without having to load an 36 | | entire folder. Here you can specify which commands should be added to 37 | | your list of commands. The console's kernel will try to load them. 38 | | 39 | */ 40 | 41 | 'add' => [ 42 | // .. 43 | ], 44 | 45 | /* 46 | |-------------------------------------------------------------------------- 47 | | Hidden Commands 48 | |-------------------------------------------------------------------------- 49 | | 50 | | Your application commands will always be visible on the application list 51 | | of commands. But you can still make them "hidden" specifying an array 52 | | of commands below. All "hidden" commands can still be run/executed. 53 | | 54 | */ 55 | 56 | 'hidden' => [ 57 | NunoMaduro\LaravelConsoleSummary\SummaryCommand::class, 58 | Symfony\Component\Console\Command\HelpCommand::class, 59 | Illuminate\Console\Scheduling\ScheduleRunCommand::class, 60 | Illuminate\Console\Scheduling\ScheduleFinishCommand::class, 61 | Illuminate\Foundation\Console\VendorPublishCommand::class, 62 | \LaravelZero\Framework\Commands\StubPublishCommand::class, 63 | \LaravelZero\Framework\Commands\BuildCommand::class, 64 | \LaravelZero\Framework\Commands\InstallCommand::class, 65 | \LaravelZero\Framework\Commands\MakeCommand::class, 66 | \LaravelZero\Framework\Commands\RenameCommand::class, 67 | \NunoMaduro\Collision\Adapters\Laravel\Commands\TestCommand::class, 68 | ], 69 | 70 | /* 71 | |-------------------------------------------------------------------------- 72 | | Removed Commands 73 | |-------------------------------------------------------------------------- 74 | | 75 | | Do you have a service provider that loads a list of commands that 76 | | you don't need? No problem. Laravel Zero allows you to specify 77 | | below a list of commands that you don't to see in your app. 78 | | 79 | */ 80 | 81 | 'remove' => [ 82 | // .. 83 | ], 84 | ]; 85 | -------------------------------------------------------------------------------- /config/logging.php: -------------------------------------------------------------------------------- 1 | env('LOG_CHANNEL', 'stack'), 21 | 22 | /* 23 | |-------------------------------------------------------------------------- 24 | | Log Channels 25 | |-------------------------------------------------------------------------- 26 | | 27 | | Here you may configure the log channels for your application. Out of 28 | | the box, Laravel uses the Monolog PHP logging library. This gives 29 | | you a variety of powerful log handlers / formatters to utilize. 30 | | 31 | | Available Drivers: "single", "daily", "slack", "syslog", 32 | | "errorlog", "monolog", 33 | | "custom", "stack" 34 | | 35 | */ 36 | 37 | 'channels' => [ 38 | 'stack' => [ 39 | 'driver' => 'stack', 40 | 'channels' => ['stderr'], 41 | 'ignore_exceptions' => false, 42 | ], 43 | 44 | 'single' => [ 45 | 'driver' => 'single', 46 | 'path' => storage_path('logs/laravel.log'), 47 | 'level' => 'debug', 48 | ], 49 | 50 | 'daily' => [ 51 | 'driver' => 'daily', 52 | 'path' => storage_path('logs/laravel.log'), 53 | 'level' => 'debug', 54 | 'days' => 14, 55 | ], 56 | 57 | 'slack' => [ 58 | 'driver' => 'slack', 59 | 'url' => env('LOG_SLACK_WEBHOOK_URL'), 60 | 'username' => 'Laravel Log', 61 | 'emoji' => ':boom:', 62 | 'level' => 'critical', 63 | ], 64 | 65 | 'papertrail' => [ 66 | 'driver' => 'monolog', 67 | 'level' => 'debug', 68 | 'handler' => SyslogUdpHandler::class, 69 | 'handler_with' => [ 70 | 'host' => env('PAPERTRAIL_URL'), 71 | 'port' => env('PAPERTRAIL_PORT'), 72 | ], 73 | ], 74 | 75 | 'stderr' => [ 76 | 'driver' => 'monolog', 77 | 'handler' => StreamHandler::class, 78 | 'formatter' => env('LOG_STDERR_FORMATTER'), 79 | 'with' => [ 80 | 'stream' => 'php://stderr', 81 | ], 82 | ], 83 | 84 | 'syslog' => [ 85 | 'driver' => 'syslog', 86 | 'level' => 'debug', 87 | ], 88 | 89 | 'errorlog' => [ 90 | 'driver' => 'errorlog', 91 | 'level' => 'debug', 92 | ], 93 | 94 | 'null' => [ 95 | 'driver' => 'monolog', 96 | 'handler' => NullHandler::class, 97 | ], 98 | 99 | 'emergency' => [ 100 | 'path' => storage_path('logs/laravel.log'), 101 | ], 102 | ], 103 | 104 | ]; 105 | -------------------------------------------------------------------------------- /config/logo.php: -------------------------------------------------------------------------------- 1 | true, 16 | 17 | /* 18 | |-------------------------------------------------------------------------- 19 | | Default Font 20 | |-------------------------------------------------------------------------- 21 | | 22 | | This option defines the font which should be used for rendering. 23 | | By default, one default font is shipped. However, you are free 24 | | to download and use additional fonts: http://www.figlet.org. 25 | | 26 | */ 27 | 28 | 'font' => base_path('resources/fonts/collosal.flf'), 29 | 30 | /* 31 | |-------------------------------------------------------------------------- 32 | | Output Width 33 | |-------------------------------------------------------------------------- 34 | | 35 | | This option defines the maximum width of the output string. This is 36 | | used for word-wrap as well as justification. Be careful when using 37 | | small values, because they may result in an undefined behavior. 38 | | 39 | */ 40 | 41 | 'outputWidth' => 80, 42 | 43 | /* 44 | |-------------------------------------------------------------------------- 45 | | Justification 46 | |-------------------------------------------------------------------------- 47 | | 48 | | This option defines the justification of the logo text. By default, 49 | | justification is provided, which will work well on most of your 50 | | console apps. Of course, you are free to change this value. 51 | | 52 | */ 53 | 54 | 'justification' => null, 55 | 56 | /* 57 | |-------------------------------------------------------------------------- 58 | | Right To Left 59 | |-------------------------------------------------------------------------- 60 | | 61 | | This option defines the option in which the text is written. By, default 62 | | the setting of the font-file is used. When justification is not defined, 63 | | a text written from right-to-left is automatically right-aligned. 64 | | 65 | | Possible values: "right-to-left", "left-to-right", null 66 | | 67 | */ 68 | 69 | 'rightToLeft' => null, 70 | ]; 71 | -------------------------------------------------------------------------------- /eco: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | make(Illuminate\Contracts\Console\Kernel::class); 34 | 35 | $status = $kernel->handle( 36 | $input = new Symfony\Component\Console\Input\ArgvInput, 37 | new Symfony\Component\Console\Output\ConsoleOutput 38 | ); 39 | 40 | /* 41 | |-------------------------------------------------------------------------- 42 | | Shutdown The Application 43 | |-------------------------------------------------------------------------- 44 | | 45 | | Once Artisan has finished running, we will fire off the shutdown events 46 | | so that any final work may be done by the application before we shut 47 | | down the process. This is the last thing to happen to the request. 48 | | 49 | */ 50 | 51 | $kernel->terminate($input, $status); 52 | 53 | exit($status); 54 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hotmeteor/eco-cli/c9b3e81d770f7fdb3f50470c2e518c116ff76b4d/logo.png -------------------------------------------------------------------------------- /logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | ./tests/Feature 14 | 15 | 16 | ./tests/Unit 17 | 18 | 19 | 20 | 21 | ./app 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /resources/fonts/collosal.flf: -------------------------------------------------------------------------------- 1 | flf2a$ 11 8 20 32 13 2 | Colossal.flf (Jonathon - jon@mq.edu.au) 3 | 8 June 1994 4 | 5 | Explanation of first line: 6 | flf2 - "magic number" for file identification 7 | a - should always be `a', for now 8 | $ - the "hardblank" -- prints as a blank, but can't be smushed 9 | 11 - height of a character 10 | 8 - height of a character, not including descenders 11 | 20 - max line length (excluding comment lines) + a fudge factor 12 | 32 - default smushmode for this font 13 | 13 - number of comment lines 14 | 15 | $ $@ 16 | $ $@ 17 | $ $@ 18 | $ $@ 19 | $ $@ 20 | $ $@ 21 | $ $@ 22 | $ $@ 23 | $ $@ 24 | $ $@ 25 | $ $@@ 26 | 888$@ 27 | 888$@ 28 | 888$@ 29 | 888$@ 30 | 888$@ 31 | Y8P$@ 32 | " $@ 33 | 888$@ 34 | @ 35 | @ 36 | @@ 37 | 88 88$@ 38 | 8P 8P$@ 39 | " " $@ 40 | $ $ @ 41 | $ $ @ 42 | $ $ @ 43 | $ $ @ 44 | $ $ @ 45 | $ $ @ 46 | $ $ @ 47 | $ $ @@ 48 | 888 888 $@ 49 | 888 888 $@ 50 | 888888888888$@ 51 | 888 888 $@ 52 | 888 888 $@ 53 | 888888888888$@ 54 | 888 888 $@ 55 | 888 888 $@ 56 | $@ 57 | $@ 58 | $@@ 59 | 88 $@ 60 | .d88888b. $@ 61 | d88P 88"88b$@ 62 | Y88b.88 $ @ 63 | "Y88888b.$ @ 64 | 88"88b$@ 65 | Y88b 88.88P$@ 66 | "Y88888P"$ @ 67 | 88 @ 68 | @ 69 | @@ 70 | d88b d88P$@ 71 | Y88P d88P $@ 72 | d88P $ @ 73 | d88P $ @ 74 | $ d88P $ @ 75 | $ d88P $ @ 76 | $d88P d88b$@ 77 | d88P Y88P$@ 78 | @ 79 | @ 80 | @@ 81 | .d8888b. $ @ 82 | d88P "88b $ @ 83 | Y88b. d88P $ @ 84 | "Y8888P" $ @ 85 | .d88P88K.d88P$@ 86 | 888" Y888P" $@ 87 | Y88b .d8888b $@ 88 | "Y8888P" Y88b@ 89 | @ 90 | @ 91 | @@ 92 | d8b$@ 93 | 88P$@ 94 | 8P $@ 95 | " $ @ 96 | $ @ 97 | $ @ 98 | $ @ 99 | $ @ 100 | $ @ 101 | $ @ 102 | $ @@ 103 | $.d88$@ 104 | $d88P"$@ 105 | d88P $ @ 106 | 888 $ @ 107 | 888 $ @ 108 | Y88b $ @ 109 | $Y88b.$@ 110 | $"Y88$@ 111 | @ 112 | @ 113 | @@ 114 | 88b.$ @ 115 | "Y88b$ @ 116 | Y88b$@ 117 | 888$@ 118 | 888$@ 119 | d88P$@ 120 | .d88P$ @ 121 | 88P"$ @ 122 | @ 123 | @ 124 | @@ 125 | @ 126 | o $ @ 127 | d8b $ @ 128 | d888b $ @ 129 | "Y888888888P"@ 130 | "Y88888P"$ @ 131 | d88P"Y88b $@ 132 | dP" "Yb$@ 133 | @ 134 | @ 135 | @@ 136 | $ @ 137 | $ @ 138 | $ @ 139 | 888 $@ 140 | 8888888$@ 141 | 888 $@ 142 | $ @ 143 | $ @ 144 | $ @ 145 | $ @ 146 | $ @@ 147 | $ @ 148 | $ @ 149 | $ @ 150 | $ @ 151 | $ @ 152 | $ @ 153 | d8b$@ 154 | 88P$@ 155 | 8P @ 156 | " @ 157 | @@ 158 | $ $ @ 159 | $ $ @ 160 | $ $ @ 161 | $ $ @ 162 | $ $ @ 163 | 888888$@ 164 | $ $ @ 165 | $ $ @ 166 | $ $ @ 167 | $ $ @ 168 | $ $ @@ 169 | $ @ 170 | $ @ 171 | $ @ 172 | $ @ 173 | $ @ 174 | $ @ 175 | d8b$@ 176 | Y8P$@ 177 | @ 178 | @ 179 | @@ 180 | $ d88P$@ 181 | $ d88P $@ 182 | $ d88P $ @ 183 | $ d88P $ @ 184 | $ d88P $ @ 185 | $ d88P $ @ 186 | $d88P $ @ 187 | d88P $ @ 188 | @ 189 | @ 190 | @@ 191 | $.d8888b.$ @ 192 | d88P Y88b$@ 193 | 888 888$@ 194 | 888 888$@ 195 | 888 888$@ 196 | 888 888$@ 197 | Y88b d88P$@ 198 | $"Y8888P"$ @ 199 | @ 200 | @ 201 | @@ 202 | d888 $ @ 203 | d8888 $ @ 204 | 888 $ @ 205 | 888 $ @ 206 | 888 $ @ 207 | 888 $ @ 208 | 888 $ @ 209 | 8888888$@ 210 | @ 211 | @ 212 | @@ 213 | .d8888b.$ @ 214 | d88P Y88b$@ 215 | $ 888$@ 216 | $ .d88P$@ 217 | .od888P" $@ 218 | d88P" $@ 219 | 888" $@ 220 | 888888888 $@ 221 | @ 222 | @ 223 | @@ 224 | .d8888b.$ @ 225 | d88P Y88b$@ 226 | $ .d88P$@ 227 | $ 8888" $@ 228 | $ "Y8b.$@ 229 | 888 888$@ 230 | Y88b d88P$@ 231 | "Y8888P" $@ 232 | @ 233 | @ 234 | @@ 235 | d8888 $@ 236 | d8P888 $@ 237 | d8P 888 $@ 238 | d8P 888 $@ 239 | d88 888 $@ 240 | 8888888888$@ 241 | 888 $@ 242 | 888 $@ 243 | @ 244 | @ 245 | @@ 246 | 888888888$ @ 247 | 888 $ @ 248 | 888 $ @ 249 | 8888888b.$ @ 250 | $ "Y88b$@ 251 | $ 888$@ 252 | Y88b d88P$@ 253 | "Y8888P"$ @ 254 | @ 255 | @ 256 | @@ 257 | $.d8888b.$ @ 258 | d88P Y88b$@ 259 | 888 $ @ 260 | 888d888b.$ @ 261 | 888P "Y88b$@ 262 | 888 888$@ 263 | Y88b d88P$@ 264 | $"Y8888P"$ @ 265 | @ 266 | @ 267 | @@ 268 | 8888888888$@ 269 | $ d88P$@ 270 | $ d88P $@ 271 | $ d88P $ @ 272 | $88888888$ @ 273 | $ d88P $ @ 274 | $d88P $ @ 275 | d88P $ @ 276 | @ 277 | @ 278 | @@ 279 | .d8888b.$ @ 280 | d88P Y88b$@ 281 | Y88b. d88P$@ 282 | "Y88888" $@ 283 | .d8P""Y8b.$@ 284 | 888 888$@ 285 | Y88b d88P$@ 286 | "Y8888P" $@ 287 | @ 288 | @ 289 | @@ 290 | $.d8888b.$ @ 291 | d88P Y88b$@ 292 | 888 888$@ 293 | Y88b. d888$@ 294 | $"Y888P888$@ 295 | $ 888$@ 296 | Y88b d88P$@ 297 | "Y8888P"$ @ 298 | @ 299 | @ 300 | @@ 301 | $ @ 302 | $ @ 303 | $ @ 304 | d8b$@ 305 | Y8P$@ 306 | $ @ 307 | d8b$@ 308 | Y8P$@ 309 | @ 310 | @ 311 | @@ 312 | $ @ 313 | $ @ 314 | $ @ 315 | d8b @ 316 | Y8P @ 317 | $ @ 318 | d8b$@ 319 | 88P$@ 320 | 8P @ 321 | " @ 322 | @@ 323 | $ d88P$@ 324 | $ d88P $@ 325 | d88P $ @ 326 | d88P $ @ 327 | Y88b $ @ 328 | Y88b $ @ 329 | $ Y88b $@ 330 | $ Y88b$@ 331 | @ 332 | @ 333 | @@ 334 | $ $ @ 335 | $ $ @ 336 | $ $ @ 337 | 888888$@ 338 | $ $ @ 339 | 888888$@ 340 | $ $ @ 341 | $ $ @ 342 | $ $ @ 343 | $ $ @ 344 | $ $ @@ 345 | Y88b $ @ 346 | Y88b $ @ 347 | Y88b $@ 348 | Y88b$@ 349 | d88P$@ 350 | d88P $@ 351 | d88P $ @ 352 | d88P $ @ 353 | @ 354 | @ 355 | @@ 356 | $.d8888b.$ @ 357 | d88P Y88b$@ 358 | $ .d88P$@ 359 | $ .d88P"$ @ 360 | $ 888" $ @ 361 | $ 888 $ @ 362 | $ $ @ 363 | $ 888 $ @ 364 | @ 365 | @ 366 | @@ 367 | $.d8888888b.$ @ 368 | d88P" "Y88b$@ 369 | 888 d8b 888$@ 370 | 888 888 888$@ 371 | 888 888bd88P$@ 372 | 888 Y8888P" $@ 373 | Y88b. .d8$@ 374 | $"Y88888888P"$@ 375 | @ 376 | @ 377 | @@ 378 | $d8888$@ 379 | $d88888$@ 380 | $d88P888$@ 381 | $d88P 888$@ 382 | $d88P 888$@ 383 | $d88P 888$@ 384 | $d8888888888$@ 385 | d88P 888$@ 386 | @ 387 | @ 388 | @@ 389 | 888888b.$ @ 390 | 888 "88b$ @ 391 | 888 .88P$ @ 392 | 8888888K.$ @ 393 | 888 "Y88b$@ 394 | 888 888$@ 395 | 888 d88P$@ 396 | 8888888P"$ @ 397 | @ 398 | @ 399 | @@ 400 | $.d8888b.$ @ 401 | d88P Y88b$@ 402 | 888 888$@ 403 | 888 $ @ 404 | 888 $ @ 405 | 888 888$@ 406 | Y88b d88P$@ 407 | $"Y8888P"$ @ 408 | @ 409 | @ 410 | @@ 411 | 8888888b.$ @ 412 | 888 "Y88b$@ 413 | 888 888$@ 414 | 888 888$@ 415 | 888 888$@ 416 | 888 888$@ 417 | 888 .d88P$@ 418 | 8888888P"$ @ 419 | @ 420 | @ 421 | @@ 422 | 8888888888$@ 423 | 888 $ @ 424 | 888 $ @ 425 | 8888888$ @ 426 | 888 $ @ 427 | 888 $ @ 428 | 888 $ @ 429 | 8888888888$@ 430 | @ 431 | @ 432 | @@ 433 | 8888888888$@ 434 | 888 $ @ 435 | 888 $ @ 436 | 8888888$ @ 437 | 888 $ @ 438 | 888 $ @ 439 | 888 $ @ 440 | 888 $ @ 441 | @ 442 | @ 443 | @@ 444 | $.d8888b.$ @ 445 | d88P Y88b$@ 446 | 888 888$@ 447 | 888 $@ 448 | 888 88888$@ 449 | 888 888$@ 450 | Y88b d88P$@ 451 | $"Y8888P88$@ 452 | @ 453 | @ 454 | @@ 455 | 888 888$@ 456 | 888 888$@ 457 | 888 888$@ 458 | 8888888888$@ 459 | 888 888$@ 460 | 888 888$@ 461 | 888 888$@ 462 | 888 888$@ 463 | @ 464 | @ 465 | @@ 466 | 8888888$@ 467 | 888 $ @ 468 | 888 $ @ 469 | 888 $ @ 470 | 888 $ @ 471 | 888 $ @ 472 | 888 $ @ 473 | 8888888$@ 474 | @ 475 | @ 476 | @@ 477 | 888888$@ 478 | "88b$@ 479 | 888$@ 480 | 888$@ 481 | 888$@ 482 | 888$@ 483 | 88P$@ 484 | 888$@ 485 | .d88P$@ 486 | .d88P"$ @ 487 | 888P" $ @@ 488 | 888 d8P$ @ 489 | 888 d8P $ @ 490 | 888 d8P $ @ 491 | 888d88K $ @ 492 | 8888888b $ @ 493 | 888 Y88b $ @ 494 | 888 Y88b $@ 495 | 888 Y88b$@ 496 | @ 497 | @ 498 | @@ 499 | 888 $ @ 500 | 888 $ @ 501 | 888 $ @ 502 | 888 $ @ 503 | 888 $ @ 504 | 888 $ @ 505 | 888 $ @ 506 | 88888888$@ 507 | @ 508 | @ 509 | @@ 510 | 888b d888$@ 511 | 8888b d8888$@ 512 | 88888b.d88888$@ 513 | 888Y88888P888$@ 514 | 888 Y888P 888$@ 515 | 888 Y8P 888$@ 516 | 888 " 888$@ 517 | 888 888$@ 518 | @ 519 | @ 520 | @@ 521 | 888b 888$@ 522 | 8888b 888$@ 523 | 88888b 888$@ 524 | 888Y88b 888$@ 525 | 888 Y88b888$@ 526 | 888 Y88888$@ 527 | 888 Y8888$@ 528 | 888 Y888$@ 529 | @ 530 | @ 531 | @@ 532 | $.d88888b.$ @ 533 | d88P" "Y88b$@ 534 | 888 888$@ 535 | 888 888$@ 536 | 888 888$@ 537 | 888 888$@ 538 | Y88b. .d88P$@ 539 | $"Y88888P"$ @ 540 | @ 541 | @ 542 | @@ 543 | 8888888b.$ @ 544 | 888 Y88b$@ 545 | 888 888$@ 546 | 888 d88P$@ 547 | 8888888P"$ @ 548 | 888 $ @ 549 | 888 $ @ 550 | 888 $ @ 551 | @ 552 | @ 553 | @@ 554 | $.d88888b.$ @ 555 | d88P" "Y88b$@ 556 | 888 888$@ 557 | 888 888$@ 558 | 888 888$@ 559 | 888 Y8b 888$@ 560 | Y88b.Y8b88P$@ 561 | $"Y888888" $@ 562 | Y8b $@ 563 | @ 564 | @@ 565 | 8888888b.$ @ 566 | 888 Y88b$@ 567 | 888 888$@ 568 | 888 d88P$@ 569 | 8888888P"$ @ 570 | 888 T88b $ @ 571 | 888 T88b$ @ 572 | 888 T88b$@ 573 | @ 574 | @ 575 | @@ 576 | $.d8888b.$ @ 577 | d88P Y88b$@ 578 | Y88b. $ @ 579 | $"Y888b. $ @ 580 | $ "Y88b.$@ 581 | $ "888$@ 582 | Y88b d88P$@ 583 | "Y8888P"$ @ 584 | @ 585 | @ 586 | @@ 587 | 88888888888$@ 588 | 888 $ @ 589 | 888 $ @ 590 | 888 $ @ 591 | 888 $ @ 592 | 888 $ @ 593 | 888 $ @ 594 | 888 $ @ 595 | @ 596 | @ 597 | @@ 598 | 888 888$@ 599 | 888 888$@ 600 | 888 888$@ 601 | 888 888$@ 602 | 888 888$@ 603 | 888 888$@ 604 | Y88b. .d88P$@ 605 | $"Y88888P"$ @ 606 | @ 607 | @ 608 | @@ 609 | 888 888$@ 610 | 888 888$@ 611 | 888 888$@ 612 | Y88b d88P$@ 613 | Y88b d88P $@ 614 | Y88o88P $ @ 615 | Y888P $ @ 616 | Y8P $ @ 617 | @ 618 | @ 619 | @@ 620 | 888 888$@ 621 | 888 o 888$@ 622 | 888 d8b 888$@ 623 | 888 d888b 888$@ 624 | 888d88888b888$@ 625 | 88888P Y88888$@ 626 | 8888P Y8888$@ 627 | 888P Y888$@ 628 | @ 629 | @ 630 | @@ 631 | Y88b d88P$@ 632 | Y88b d88P $@ 633 | Y88o88P $ @ 634 | Y888P $ @ 635 | d888b $ @ 636 | d88888b $ @ 637 | d88P Y88b $@ 638 | d88P Y88b$@ 639 | @ 640 | @ 641 | @@ 642 | Y88b d88P$@ 643 | Y88b d88P $@ 644 | Y88o88P $ @ 645 | Y888P $ @ 646 | 888 $ @ 647 | 888 $ @ 648 | 888 $ @ 649 | 888 $ @ 650 | @ 651 | @ 652 | @@ 653 | 8888888888P$@ 654 | $ d88P $@ 655 | $ d88P $ @ 656 | $ d88P $ @ 657 | $ d88P $ @ 658 | $ d88P $ @ 659 | $d88P $ @ 660 | d8888888888$@ 661 | @ 662 | @ 663 | @@ 664 | 8888888$@ 665 | 888 $ @ 666 | 888 $ @ 667 | 888 $ @ 668 | 888 $ @ 669 | 888 $ @ 670 | 888 $ @ 671 | 8888888$@ 672 | @ 673 | @ 674 | @@ 675 | Y88b $ @ 676 | $Y88b $ @ 677 | $ Y88b $ @ 678 | $ Y88b $ @ 679 | $ Y88b $ @ 680 | $ Y88b $ @ 681 | $ Y88b $@ 682 | $ Y88b$@ 683 | @ 684 | @ 685 | @@ 686 | 8888888$@ 687 | $ 888$@ 688 | $ 888$@ 689 | $ 888$@ 690 | $ 888$@ 691 | $ 888$@ 692 | $ 888$@ 693 | 8888888$@ 694 | @ 695 | @ 696 | @@ 697 | o$ @ 698 | d8b$ @ 699 | d888b$ @ 700 | d8P"Y8b$@ 701 | $ $ @ 702 | $ $ @ 703 | $ $ @ 704 | $ $ @ 705 | $ $ @ 706 | $ $ @ 707 | $ $ @@ 708 | $ $ @ 709 | $ $ @ 710 | $ $ @ 711 | $ $ @ 712 | $ $ @ 713 | $ $ @ 714 | $ $ @ 715 | 88888888$@ 716 | @ 717 | @ 718 | @@ 719 | d8b$@ 720 | Y88$@ 721 | Y8$@ 722 | Y$@ 723 | $ @ 724 | $ @ 725 | $ @ 726 | $ @ 727 | $ @ 728 | $ @ 729 | $ @@ 730 | @ 731 | @ 732 | @ 733 | $8888b. $@ 734 | $ "88b$@ 735 | .d888888$@ 736 | 888 888$@ 737 | "Y888888$@ 738 | @ 739 | @ 740 | @@ 741 | 888 $ @ 742 | 888 $ @ 743 | 888 $ @ 744 | 88888b.$ @ 745 | 888 "88b$@ 746 | 888 888$@ 747 | 888 d88P$@ 748 | 88888P"$ @ 749 | @ 750 | @ 751 | @@ 752 | @ 753 | @ 754 | @ 755 | $.d8888b$@ 756 | d88P" $ @ 757 | 888 $ @ 758 | Y88b. $ @ 759 | $"Y8888P$@ 760 | @ 761 | @ 762 | @@ 763 | 888$@ 764 | 888$@ 765 | 888$@ 766 | $.d88888$@ 767 | d88" 888$@ 768 | 888 888$@ 769 | Y88b 888$@ 770 | $"Y88888$@ 771 | @ 772 | @ 773 | @@ 774 | @ 775 | @ 776 | @ 777 | $.d88b.$ @ 778 | d8P Y8b$@ 779 | 88888888$@ 780 | Y8b.$ @ 781 | $"Y8888$ @ 782 | @ 783 | @ 784 | @@ 785 | $.d888$@ 786 | d88P"$ @ 787 | 888 $ @ 788 | 888888$@ 789 | 888 $ @ 790 | 888 $ @ 791 | 888 $ @ 792 | 888 $ @ 793 | @ 794 | @ 795 | @@ 796 | @ 797 | @ 798 | @ 799 | $.d88b.$ @ 800 | d88P"88b$@ 801 | 888 888$@ 802 | Y88b 888$@ 803 | $"Y88888$@ 804 | $ 888$@ 805 | Y8b d88P$@ 806 | "Y88P"$ @@ 807 | 888 $ @ 808 | 888 $ @ 809 | 888 $ @ 810 | 88888b.$ @ 811 | 888 "88b$@ 812 | 888 888$@ 813 | 888 888$@ 814 | 888 888$@ 815 | @ 816 | @ 817 | @@ 818 | d8b$@ 819 | Y8P$@ 820 | $ $@ 821 | 888$@ 822 | 888$@ 823 | 888$@ 824 | 888$@ 825 | 888$@ 826 | @ 827 | @ 828 | @@ 829 | $d8b$@ 830 | $Y8P$@ 831 | $ $@ 832 | $8888$@ 833 | $"888$@ 834 | $ 888$@ 835 | $ 888$@ 836 | $ 888$@ 837 | $ 888$@ 838 | $ d88P$@ 839 | 888P"$ @@ 840 | 888 $ @ 841 | 888 $ @ 842 | 888 $ @ 843 | 888 888$@ 844 | 888 .88P$@ 845 | 888888K$ @ 846 | 888 "88b$@ 847 | 888 888$@ 848 | @ 849 | @ 850 | @@ 851 | 888$@ 852 | 888$@ 853 | 888$@ 854 | 888$@ 855 | 888$@ 856 | 888$@ 857 | 888$@ 858 | 888$@ 859 | @ 860 | @ 861 | @@ 862 | @ 863 | @ 864 | @ 865 | 88888b.d88b.$ @ 866 | 888 "888 "88b$@ 867 | 888 888 888$@ 868 | 888 888 888$@ 869 | 888 888 888$@ 870 | @ 871 | @ 872 | @@ 873 | @ 874 | @ 875 | @ 876 | 88888b.$ @ 877 | 888 "88b$@ 878 | 888 888$@ 879 | 888 888$@ 880 | 888 888$@ 881 | @ 882 | @ 883 | @@ 884 | @ 885 | @ 886 | @ 887 | $.d88b.$ @ 888 | d88""88b$@ 889 | 888 888$@ 890 | Y88..88P$@ 891 | $"Y88P"$ @ 892 | @ 893 | @ 894 | @@ 895 | @ 896 | @ 897 | @ 898 | 88888b.$ @ 899 | 888 "88b$@ 900 | 888 888$@ 901 | 888 d88P$@ 902 | 88888P"$ @ 903 | 888 $ @ 904 | 888 $ @ 905 | 888 $ @@ 906 | @ 907 | @ 908 | @ 909 | $.d88888$@ 910 | d88" 888$@ 911 | 888 888$@ 912 | Y88b 888$@ 913 | $"Y88888$@ 914 | $ 888$@ 915 | $ 888$@ 916 | $ 888$@@ 917 | @ 918 | @ 919 | @ 920 | 888d888$@ 921 | 888P"$ @ 922 | 888 $ @ 923 | 888 $ @ 924 | 888 $ @ 925 | @ 926 | @ 927 | @@ 928 | @ 929 | @ 930 | @ 931 | .d8888b$ @ 932 | 88K $ @ 933 | "Y8888b.$@ 934 | $ X88$@ 935 | $88888P'$@ 936 | @ 937 | @ 938 | @@ 939 | 888 $ @ 940 | 888 $ @ 941 | 888 $ @ 942 | 888888$@ 943 | 888 $ @ 944 | 888 $ @ 945 | Y88b.$ @ 946 | "Y888$@ 947 | @ 948 | @ 949 | @@ 950 | @ 951 | @ 952 | @ 953 | 888 888$@ 954 | 888 888$@ 955 | 888 888$@ 956 | Y88b 888$@ 957 | $"Y88888$@ 958 | @ 959 | @ 960 | @@ 961 | @ 962 | @ 963 | @ 964 | 888 888$@ 965 | 888 888$@ 966 | Y88 88P$@ 967 | $Y8bd8P$ @ 968 | $ Y88P $ @ 969 | @ 970 | @ 971 | @@ 972 | @ 973 | @ 974 | @ 975 | 888 888 888$@ 976 | 888 888 888$@ 977 | 888 888 888$@ 978 | Y88b 888 d88P$@ 979 | $"Y8888888P"$ @ 980 | @ 981 | @ 982 | @@ 983 | @ 984 | @ 985 | @ 986 | 888 888$@ 987 | `Y8bd8P'$@ 988 | $ X88K $ @ 989 | .d8""8b.$@ 990 | 888 888$@ 991 | @ 992 | @ 993 | @@ 994 | @ 995 | @ 996 | @ 997 | 888 888$@ 998 | 888 888$@ 999 | 888 888$@ 1000 | Y88b 888$@ 1001 | $"Y88888$@ 1002 | $ 888$@ 1003 | Y8b d88P$@ 1004 | $"Y88P"$ @@ 1005 | @ 1006 | @ 1007 | @ 1008 | 88888888$@ 1009 | $ d88P $@ 1010 | $ d88P $ @ 1011 | $d88P $ @ 1012 | 88888888$@ 1013 | @ 1014 | @ 1015 | @@ 1016 | $.d888$@ 1017 | $d88P"$ @ 1018 | $888 $ @ 1019 | .888 $ @ 1020 | 888( $ @ 1021 | "888 $ @ 1022 | $888 $ @ 1023 | $Y88b.$ @ 1024 | $"Y888$@ 1025 | @ 1026 | @@ 1027 | $ 888 $@ 1028 | $ 888 $@ 1029 | $ 888 $@ 1030 | $ 888 $@ 1031 | $ $@ 1032 | $ 888 $@ 1033 | $ 888 $@ 1034 | $ 888 $@ 1035 | $ 888 $@ 1036 | @ 1037 | @@ 1038 | 888b. $ @ 1039 | $"Y88b $@ 1040 | $ 888 $@ 1041 | $ 888.$@ 1042 | $ )888$@ 1043 | $ 888"$@ 1044 | $ 888 $@ 1045 | $.d88P $@ 1046 | 888P" $ @ 1047 | @ 1048 | @@ 1049 | @ 1050 | @ 1051 | $d888b d88$@ 1052 | d888888888P$@ 1053 | 88P Y888P$ @ 1054 | @ 1055 | @ 1056 | @ 1057 | @ 1058 | @ 1059 | @@ 1060 | d8b d8b@ 1061 | Y8P Y8P@ 1062 | $d88888$@ 1063 | $d88P888$@ 1064 | $d88P 888$@ 1065 | $d88P 888$@ 1066 | $d888888888$@ 1067 | $d88P 888$@ 1068 | @ 1069 | @ 1070 | @@ 1071 | d8b d8b @ 1072 | Y8P Y8P @ 1073 | $.d88888b.$ @ 1074 | d88P" "Y88b$@ 1075 | 888 888$@ 1076 | 888 888$@ 1077 | Y88b. .d88P$@ 1078 | $"Y88888P"$ @ 1079 | @ 1080 | @ 1081 | @@ 1082 | d8b d8b @ 1083 | Y8P Y8P @ 1084 | 888 888$@ 1085 | 888 888$@ 1086 | 888 888$@ 1087 | 888 888$@ 1088 | Y88b. .d88P$@ 1089 | $"Y88888P"$ @ 1090 | @ 1091 | @ 1092 | @@ 1093 | d8b d8b @ 1094 | Y8P Y8P @ 1095 | @ 1096 | $8888b. $@ 1097 | $ "88b$@ 1098 | .d888888$@ 1099 | 888 888$@ 1100 | "Y888888$@ 1101 | @ 1102 | @ 1103 | @@ 1104 | d8b d8b @ 1105 | Y8P Y8P @ 1106 | @ 1107 | $.d88b.$ @ 1108 | d88""88b$@ 1109 | 888 888$@ 1110 | Y88..88P$@ 1111 | $"Y88P"$ @ 1112 | @ 1113 | @ 1114 | @@ 1115 | d8b d8b @ 1116 | Y8P Y8P @ 1117 | @ 1118 | 888 888$@ 1119 | 888 888$@ 1120 | 888 888$@ 1121 | Y88b 888$@ 1122 | $"Y88888$@ 1123 | @ 1124 | @ 1125 | @@ 1126 | .d888b.$ @ 1127 | d88" "88b$ @ 1128 | 888 .88P$ @ 1129 | 888 888K.$ @ 1130 | 888 "Y88b$@ 1131 | 888 888$@ 1132 | 888 d88P$@ 1133 | 888 888P"$ @ 1134 | 888 @ 1135 | 888 @ 1136 | @@ 1137 | -------------------------------------------------------------------------------- /tests/CreatesApplication.php: -------------------------------------------------------------------------------- 1 | make(Kernel::class)->bootstrap(); 19 | 20 | return $app; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/Feature/EnvFreshCommandTest.php: -------------------------------------------------------------------------------- 1 | artisan('env:fresh') 20 | ->expectsConfirmation('Are you sure you want a fresh .env? This will overwrite your existing .env file.', 'no') 21 | ->assertExitCode(0); 22 | 23 | $this->assertStringEqualsFile(self::envFile(), 'MY=value1234'.PHP_EOL); 24 | } 25 | 26 | public function test_should_refresh_with_confirmation() 27 | { 28 | $mock = $this->mockDriver(); 29 | 30 | $mock->expects($this->once()) 31 | ->method('getRemoteFile') 32 | ->willReturn(new File('KEY=value'.PHP_EOL, 'hash')); 33 | 34 | $this->artisan('env:fresh') 35 | ->expectsConfirmation('Are you sure you want a fresh .env? This will overwrite your existing .env file.', 'yes') 36 | ->assertExitCode(0); 37 | 38 | $this->assertStringEqualsFile(self::envFile(), 'KEY=value'.PHP_EOL); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/Feature/EnvPushCommandTest.php: -------------------------------------------------------------------------------- 1 | mockDriver(); 24 | 25 | $this->mockAuthenticate($mock); 26 | 27 | $mock->expects($this->once()) 28 | ->method('getSecretKey') 29 | ->willReturn(['key' => 'key']); 30 | 31 | $mock->expects($this->once()) 32 | ->method('getRemoteFile') 33 | ->willReturn(new File('', null)); 34 | 35 | $mock->expects($this->once()) 36 | ->method('decryptContents') 37 | ->willReturn([]); 38 | 39 | $mock->expects($this->once()) 40 | ->method('encryptContents') 41 | ->willReturn('encrypted'); 42 | 43 | $mock->expects($this->once()) 44 | ->method('createRemoteFile') 45 | ->willReturn(null); 46 | 47 | $this->artisan('env:push') 48 | ->expectsQuestion('What key should be pushed?', 'KEY') 49 | ->expectsQuestion('What is the value?', 'value') 50 | ->expectsOutput('The value was successfully added to the .eco file.') 51 | ->assertExitCode(0); 52 | 53 | $this->assertStringEqualsFile(self::envFile(), ''); 54 | } 55 | 56 | public function test_should_push_and_create_key() 57 | { 58 | $mock = $this->mockDriver(); 59 | 60 | $mock->expects($this->once()) 61 | ->method('getSecretKey') 62 | ->willReturn(['key' => 'key']); 63 | 64 | $mock->expects($this->once()) 65 | ->method('getRemoteFile') 66 | ->willReturn(new File('ANOTHER=value'.PHP_EOL, 'hash')); 67 | 68 | $mock->expects($this->once()) 69 | ->method('decryptContents') 70 | ->willReturn(['ANOTHER' => 'value']); 71 | 72 | $mock->expects($this->once()) 73 | ->method('encryptContents') 74 | ->willReturn('encrypted'); 75 | 76 | $mock->expects($this->once()) 77 | ->method('updateRemoteFile') 78 | ->willReturn(null); 79 | 80 | $this->artisan('env:push') 81 | ->expectsQuestion('What key should be pushed?', 'KEY') 82 | ->expectsQuestion('What is the value?', 'value') 83 | ->assertExitCode(0); 84 | 85 | $this->assertStringEqualsFile(self::envFile(), ''); 86 | } 87 | 88 | public function test_should_push_and_update_key_with_confirmation() 89 | { 90 | $mock = $this->mockDriver(); 91 | 92 | $mock->expects($this->once()) 93 | ->method('getSecretKey') 94 | ->willReturn(['key' => 'key']); 95 | 96 | $mock->expects($this->once()) 97 | ->method('getRemoteFile') 98 | ->willReturn(new File('KEY=value'.PHP_EOL, 'hash')); 99 | 100 | $mock->expects($this->once()) 101 | ->method('decryptContents') 102 | ->willReturn(['KEY' => 'value']); 103 | 104 | $mock->expects($this->once()) 105 | ->method('encryptContents') 106 | ->willReturn('encrypted'); 107 | 108 | $mock->expects($this->once()) 109 | ->method('updateRemoteFile') 110 | ->willReturn(null); 111 | 112 | $this->artisan('env:push') 113 | ->expectsQuestion('What key should be pushed?', 'KEY') 114 | ->expectsQuestion('What is the value?', 'value') 115 | ->expectsConfirmation('This environment key already exists. Are you sure you want to change it?', 'yes') 116 | ->expectsOutput('The value was successfully added to the .eco file.') 117 | ->assertExitCode(0); 118 | 119 | $this->assertStringEqualsFile(self::envFile(), ''); 120 | } 121 | 122 | public function test_should_push_and_not_update_key_without_confirmation() 123 | { 124 | $mock = $this->mockDriver(); 125 | 126 | $mock->expects($this->once()) 127 | ->method('getSecretKey') 128 | ->willReturn(['key' => 'key']); 129 | 130 | $mock->expects($this->once()) 131 | ->method('getRemoteFile') 132 | ->willReturn(new File('KEY=value'.PHP_EOL, null)); 133 | 134 | $mock->expects($this->once()) 135 | ->method('decryptContents') 136 | ->willReturn(['KEY' => 'value']); 137 | 138 | $mock->expects($this->never()) 139 | ->method('encryptContents') 140 | ->willReturn('encrypted'); 141 | 142 | $mock->expects($this->never()) 143 | ->method('createRemoteFile') 144 | ->willReturn(null); 145 | 146 | $this->artisan('env:push') 147 | ->expectsQuestion('What key should be pushed?', 'KEY') 148 | ->expectsQuestion('What is the value?', 'value') 149 | ->expectsConfirmation('This environment key already exists. Are you sure you want to change it?', 'no') 150 | ->assertExitCode(0); 151 | 152 | $this->assertStringEqualsFile(self::envFile(), ''); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /tests/Feature/EnvSetCommandTest.php: -------------------------------------------------------------------------------- 1 | artisan('env:set') 19 | ->expectsQuestion('What key should be set?', 'KEY') 20 | ->expectsQuestion('What is the value?', 'value') 21 | ->expectsOutput('The KEY value has been stored and added to your .env file.') 22 | ->assertExitCode(0); 23 | 24 | $this->assertStringEqualsFile(self::envFile(), 'KEY=value'.PHP_EOL); 25 | } 26 | 27 | public function test_it_sets_with_provided_key() 28 | { 29 | $this->artisan('env:set KEY') 30 | ->expectsQuestion('What is the value?', 'value') 31 | ->expectsOutput('The KEY value has been stored and added to your .env file.') 32 | ->assertExitCode(0); 33 | 34 | $this->assertStringEqualsFile(self::envFile(), 'KEY=value'.PHP_EOL); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/Feature/EnvSyncCommandTest.php: -------------------------------------------------------------------------------- 1 | mockDriver(); 30 | 31 | $mock->expects($this->once()) 32 | ->method('getRemoteFile') 33 | ->willReturn(new File('', null)); 34 | 35 | $mock->expects($this->once()) 36 | ->method('getSecretKey') 37 | ->willReturn(['key' => 'key']); 38 | 39 | $mock->expects($this->once()) 40 | ->method('decryptContents') 41 | ->willReturn([ 42 | 'REMOTE' => 'value', 43 | 'LEFT' => 'right', 44 | 'THIS' => '', 45 | ]); 46 | 47 | $this->artisan('env:sync') 48 | ->expectsOutput('Syncing will use your local variables, but ask you about conflicting remote variables.') 49 | ->expectsConfirmation('The THIS variable already exists in your local .env. Do you want to overwrite it?', 'yes') 50 | ->assertExitCode(0); 51 | 52 | $contents = 53 | 'KEY=value'.PHP_EOL. 54 | 'THIS='.PHP_EOL. 55 | 'UP=down'.PHP_EOL. 56 | 'REMOTE=value'.PHP_EOL. 57 | 'LEFT=right'.PHP_EOL; 58 | 59 | $this->assertStringEqualsFile(self::envFile(), $contents); 60 | } 61 | 62 | public function test_should_sync_file_without_confirming_changes() 63 | { 64 | $mock = $this->mockDriver(); 65 | 66 | $mock->expects($this->once()) 67 | ->method('getRemoteFile') 68 | ->willReturn(new File('', null)); 69 | 70 | $mock->expects($this->once()) 71 | ->method('getSecretKey') 72 | ->willReturn(['key' => 'key']); 73 | 74 | $mock->expects($this->once()) 75 | ->method('decryptContents') 76 | ->willReturn([ 77 | 'REMOTE' => 'value', 78 | 'LEFT' => 'right', 79 | 'THIS' => '', 80 | ]); 81 | 82 | $this->artisan('env:sync') 83 | ->expectsOutput('Syncing will use your local variables, but ask you about conflicting remote variables.') 84 | ->expectsConfirmation('The THIS variable already exists in your local .env. Do you want to overwrite it?', 'no') 85 | ->assertExitCode(0); 86 | 87 | $contents = 88 | 'KEY=value'.PHP_EOL. 89 | 'THIS=that'.PHP_EOL. 90 | 'UP=down'.PHP_EOL. 91 | 'REMOTE=value'.PHP_EOL. 92 | 'LEFT=right'.PHP_EOL; 93 | 94 | $this->assertStringEqualsFile(self::envFile(), $contents); 95 | } 96 | 97 | public function test_should_sync_file_and_force_changes() 98 | { 99 | $mock = $this->mockDriver(); 100 | 101 | $mock->expects($this->once()) 102 | ->method('getRemoteFile') 103 | ->willReturn(new File('', null)); 104 | 105 | $mock->expects($this->once()) 106 | ->method('getSecretKey') 107 | ->willReturn(['key' => 'key']); 108 | 109 | $mock->expects($this->once()) 110 | ->method('decryptContents') 111 | ->willReturn([ 112 | 'REMOTE' => 'value', 113 | 'LEFT' => 'right', 114 | 'THIS' => '', 115 | ]); 116 | 117 | $this->artisan('env:sync --force') 118 | ->expectsOutput('Syncing will use your local variables, but ask you about conflicting remote variables.') 119 | ->assertExitCode(0); 120 | 121 | $contents = 122 | 'KEY=value'.PHP_EOL. 123 | 'THIS='.PHP_EOL. 124 | 'UP=down'.PHP_EOL. 125 | 'REMOTE=value'.PHP_EOL. 126 | 'LEFT=right'.PHP_EOL; 127 | 128 | $this->assertStringEqualsFile(self::envFile(), $contents); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /tests/Feature/EnvUnsetCommandTest.php: -------------------------------------------------------------------------------- 1 | artisan('env:unset') 19 | ->expectsQuestion('What key should be unset?', 'KEY') 20 | ->expectsOutput('The KEY value has been deleted and removed from your .env file.') 21 | ->assertExitCode(0); 22 | 23 | $this->assertStringEqualsFile(self::envFile(), ''); 24 | } 25 | 26 | public function test_it_unsets_with_provided_key() 27 | { 28 | $this->artisan('env:unset KEY') 29 | ->expectsOutput('The KEY value has been deleted and removed from your .env file.') 30 | ->assertExitCode(0); 31 | 32 | $this->assertStringEqualsFile(self::envFile(), ''); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/Feature/InitCommandTest.php: -------------------------------------------------------------------------------- 1 | mockDriver(); 18 | 19 | $mock->expects($this->once()) 20 | ->method('authenticate') 21 | ->with('my-token') 22 | ->willReturn(null); 23 | 24 | $mock->expects($this->once()) 25 | ->method('getCurrentUser') 26 | ->willReturn(new User(1, 'hotmeteor')); 27 | 28 | $mock->expects($this->once()) 29 | ->method('getOrganizations') 30 | ->willReturn( 31 | collect([ 32 | new Organization(2222, 'ecoorg'), 33 | ]) 34 | ); 35 | 36 | $mock->expects($this->once()) 37 | ->method('getCurrentUserRepositories') 38 | ->willReturn( 39 | collect([ 40 | new Repository(3333, 'eco-cli'), 41 | ]) 42 | ); 43 | 44 | $this->artisan('init') 45 | ->expectsOutput('----') 46 | ->expectsQuestion('What code host do you use?', 'fake') 47 | ->expectsQuestion('Token', 'my-token') 48 | // ->expectsOutput('To start, you will need a Github Personal Access token.') 49 | // ->expectsOutput('https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token') 50 | ->expectsQuestion('Which organization should be used?', 1) 51 | ->expectsOutput('Organization set successfully.') 52 | ->expectsChoice('Which repository should be used? You can always switch this later.', 'eco-cli', ['eco-cli']) 53 | ->expectsOutput('Repository set successfully.'); 54 | 55 | $this->assertSame(Vault::get('driver'), 'fake'); 56 | $this->assertSame(Vault::config('token'), 'my-token'); 57 | $this->assertSame(Vault::config('org'), 'hotmeteor'); 58 | $this->assertSame(Vault::config('repo'), 'eco-cli'); 59 | } 60 | 61 | public function test_it_init_with_org() 62 | { 63 | $mock = $this->mockDriver(); 64 | 65 | $mock->expects($this->once()) 66 | ->method('authenticate') 67 | ->with('my-token') 68 | ->willReturn(null); 69 | 70 | $mock->expects($this->once()) 71 | ->method('getCurrentUser') 72 | ->willReturn(new User(1, 'hotmeteor')); 73 | 74 | $mock->expects($this->once()) 75 | ->method('getOrganizations') 76 | ->willReturn( 77 | collect([ 78 | new Organization(2222, 'ecoorg'), 79 | ]) 80 | ); 81 | 82 | $mock->expects($this->once()) 83 | ->method('getOwnerRepositories') 84 | ->willReturn( 85 | collect([ 86 | new Repository(3333, 'eco-cli'), 87 | ]) 88 | ); 89 | 90 | $this->artisan('init') 91 | ->expectsOutput('----') 92 | ->expectsQuestion('What code host do you use?', 'fake') 93 | ->expectsQuestion('Token', 'my-token') 94 | // ->expectsOutput('To start, you will need a Github Personal Access token.') 95 | // ->expectsOutput('https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token') 96 | ->expectsQuestion('Which organization should be used?', 2222) 97 | ->expectsOutput('Organization set successfully.') 98 | ->expectsChoice('Which repository should be used? You can always switch this later.', 'eco-cli', ['eco-cli']) 99 | ->expectsOutput('Repository set successfully.'); 100 | 101 | $this->assertSame(Vault::get('driver'), 'fake'); 102 | $this->assertSame(Vault::config('token'), 'my-token'); 103 | $this->assertSame(Vault::config('org'), 'ecoorg'); 104 | $this->assertSame(Vault::config('repo'), 'eco-cli'); 105 | } 106 | 107 | protected function mockDriver($driver = 'fake') 108 | { 109 | file_put_contents(self::vaultFile(), ''); 110 | 111 | $class = '\\App\\Hosts\\Drivers\\'.Str::studly("{$driver}_driver"); 112 | 113 | $mock = $this->createMock(get_class(new $class())); 114 | 115 | $manager = $this->app->get(HostManager::class); 116 | 117 | $manager->extend($driver, function () use ($mock) { 118 | return $mock; 119 | }); 120 | 121 | return $mock; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /tests/Feature/OrgCurrentCommandTest.php: -------------------------------------------------------------------------------- 1 | artisan('org:current') 15 | ->expectsOutput('You are currently working in the [hotmeteor] organization.') 16 | ->assertExitCode(0); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/Feature/OrgSwitchCommandTest.php: -------------------------------------------------------------------------------- 1 | mockDriver(); 18 | 19 | $mock->expects($this->once()) 20 | ->method('getOrganizations') 21 | ->willReturn( 22 | collect([new Organization(1, 'ecoorg')]) 23 | ); 24 | 25 | $mock->expects($this->atLeastOnce()) 26 | ->method('getCurrentUser') 27 | ->willReturn(new User(2, 'hotmeteor')); 28 | 29 | $this->artisan('org:switch') 30 | ->expectsQuestion('Which organization should be used?', 2) 31 | ->expectsOutput('Organization set successfully.') 32 | ->assertExitCode(0); 33 | 34 | $this->assertSame('hotmeteor', Vault::config('org')); 35 | $this->assertNull(Vault::config('repo')); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/Feature/RepoCurrentCommandTest.php: -------------------------------------------------------------------------------- 1 | artisan('repo:current') 15 | ->expectsOutput('You are currently working in the [eco-cli] repository.') 16 | ->assertExitCode(0); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/Feature/RepoSwitchCommandTest.php: -------------------------------------------------------------------------------- 1 | mockDriver(); 17 | $this->mockAuthenticate($mock); 18 | 19 | $mock->expects($this->once()) 20 | ->method('getCurrentUserRepositories') 21 | ->willReturn( 22 | collect([ 23 | new Repository(3333, 'eco-cli'), 24 | new Repository(4444, 'other_repo'), 25 | ]) 26 | ); 27 | 28 | $this->artisan('repo:switch') 29 | ->expectsQuestion('Which repository should be used? You can always switch this later.', 4444) 30 | ->expectsOutput('Repository set successfully.') 31 | ->assertExitCode(0); 32 | 33 | $this->assertSame('hotmeteor', Vault::config('org')); 34 | $this->assertSame('other_repo', Vault::config('repo')); 35 | } 36 | 37 | public function test_should_switch_repo_using_name() 38 | { 39 | Vault::config('org', 'hotmeteor'); 40 | Vault::config('repo', 'this_repo'); 41 | 42 | $mock = $this->mockDriver(); 43 | $this->mockAuthenticate($mock); 44 | 45 | $mock->expects($this->once()) 46 | ->method('getCurrentUserRepositories') 47 | ->willReturn( 48 | collect([ 49 | new Repository(3333, 'eco-cli'), 50 | new Repository(4444, 'other_repo'), 51 | ]) 52 | ); 53 | 54 | $this->artisan('repo:switch') 55 | ->expectsQuestion('Which repository should be used? You can always switch this later.', 'eco-cli') 56 | ->expectsOutput('Repository set successfully.') 57 | ->assertExitCode(0); 58 | 59 | $this->assertSame('hotmeteor', Vault::config('org')); 60 | $this->assertSame('eco-cli', Vault::config('repo')); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | base_path('tests/Fixtures')]); 22 | config(['app.home_path' => base_path('tests/Fixtures')]); 23 | } 24 | 25 | public static function envFile(): string 26 | { 27 | return config('app.env_path').'/.env'; 28 | } 29 | 30 | public static function vaultFile(): string 31 | { 32 | return config('app.home_path').'/.eco/config.json'; 33 | } 34 | 35 | public static function set($value): void 36 | { 37 | file_put_contents(self::envFile(), $value); 38 | } 39 | 40 | public static function reset(): void 41 | { 42 | self::set(''); 43 | } 44 | 45 | protected function mockDriver($driver = 'fake') 46 | { 47 | Vault::set('driver', $driver); 48 | Vault::config('token', 'my-token'); 49 | 50 | $class = '\\App\\Hosts\\Drivers\\'.Str::studly("{$driver}_driver"); 51 | 52 | $mock = $this->createMock(get_class(new $class())); 53 | 54 | $manager = $this->app->get(HostManager::class); 55 | 56 | $manager->extend($driver, function () use ($mock) { 57 | return $mock; 58 | }); 59 | 60 | return $mock; 61 | } 62 | 63 | protected function mockAuthenticate($mock) 64 | { 65 | $mock->expects($this->once()) 66 | ->method('authenticate') 67 | ->with('my-token') 68 | ->willReturn(null); 69 | 70 | $mock->expects($this->atLeastOnce()) 71 | ->method('getCurrentUser') 72 | ->willReturn(new User(1, 'hotmeteor')); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /tests/Unit/Support/HelpersTest.php: -------------------------------------------------------------------------------- 1 | assertSame('KEY', Helpers::formatKey('key')); 13 | $this->assertSame('KEY_NAME', Helpers::formatKey('keyName')); 14 | $this->assertSame('KEY_NAME', Helpers::formatKey('key_name')); 15 | $this->assertSame('KEY', Helpers::formatKey(' key ')); 16 | $this->assertSame('KEY_NAME', Helpers::formatKey(' keyName ')); 17 | } 18 | 19 | public function test_format_value() 20 | { 21 | $this->assertSame('value', Helpers::formatValue('value')); 22 | $this->assertSame('value', Helpers::formatValue(' value ')); 23 | $this->assertSame("'the value'", Helpers::formatValue('the value')); 24 | $this->assertSame("'the value'", Helpers::formatValue(' the value ')); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/Unit/Support/VaultTest.php: -------------------------------------------------------------------------------- 1 | contents = [ 15 | 'driver' => 'fake', 16 | 'drivers' => [ 17 | 'fake' => [ 18 | 'token' => 'ABCD-1234', 19 | 'org' => 'hotmeteor', 20 | 'repo' => 'eco-cli', 21 | ], 22 | ], 23 | 'hotmeteor' => [ 24 | 'eco-cli' => [ 25 | 'KEY' => 'value', 26 | 'THIS' => 'that', 27 | 'COLOUR' => "'light blue'", 28 | ], 29 | ], 30 | ]; 31 | 32 | file_put_contents(self::vaultFile(), json_encode($this->contents)); 33 | } 34 | 35 | public function test_vault_load() 36 | { 37 | $this->assertSame($this->contents, Vault::load()); 38 | } 39 | 40 | public function test_vault_get() 41 | { 42 | $this->assertSame('fake', Vault::get('driver')); 43 | $this->assertSame('value', Vault::get('hotmeteor.eco-cli.KEY')); 44 | } 45 | 46 | public function test_vault_set() 47 | { 48 | Vault::set('driver', 'other'); 49 | Vault::set('hotmeteor.eco-cli.KEY', 'different'); 50 | Vault::set('new', [ 51 | 'array' => 'of-values', 52 | ]); 53 | 54 | $this->assertSame('other', Vault::get('driver')); 55 | $this->assertSame('different', Vault::get('hotmeteor.eco-cli.KEY')); 56 | $this->assertSame(['array' => 'of-values'], Vault::get('new')); 57 | } 58 | 59 | public function test_vault_unset() 60 | { 61 | Vault::unset('driver'); 62 | Vault::unset('hotmeteor.eco-cli.KEY'); 63 | 64 | $this->assertArrayNotHasKey('driver', Vault::load()); 65 | $this->assertArrayHasKey('hotmeteor', Vault::load()); 66 | $this->assertArrayNotHasKey('KEY', Vault::load()['hotmeteor']['eco-cli']); 67 | } 68 | 69 | public function test_vault_config() 70 | { 71 | Vault::set('drivers.other', [ 72 | 'token' => 'EDFG-5678', 73 | 'org' => 'hotmeteor', 74 | 'repo' => 'codetown', 75 | ]); 76 | 77 | Vault::set('driver', 'fake'); 78 | 79 | $this->assertSame('ABCD-1234', Vault::config('token')); 80 | 81 | Vault::set('driver', 'other'); 82 | 83 | $this->assertSame('EDFG-5678', Vault::config('token')); 84 | } 85 | } 86 | --------------------------------------------------------------------------------