├── public └── mix-manifest.json ├── resources ├── sass │ ├── app.scss │ └── base.scss ├── js │ ├── app.js │ ├── routes.js │ ├── screens │ │ ├── repositories │ │ │ └── index.vue │ │ ├── requirements │ │ │ └── index.vue │ │ ├── jobs │ │ │ ├── item.vue │ │ │ └── index.vue │ │ ├── dashboard │ │ │ └── index.vue │ │ └── laravel │ │ │ └── index.vue │ ├── components │ │ ├── Job │ │ │ ├── StatusBadge.vue │ │ │ └── OptionsMenu.vue │ │ ├── Requirement │ │ │ ├── Requirement.vue │ │ │ ├── InstallButton.vue │ │ │ ├── UninstallButton.vue │ │ │ ├── Suggestions.vue │ │ │ └── InstallForm.vue │ │ ├── Repository │ │ │ └── Repository.vue │ │ ├── PaketCard.vue │ │ └── TopMenu.vue │ ├── globals.js │ └── store.js └── views │ └── app.blade.php ├── tailwind.js ├── contracts ├── Exceptions │ └── PaketThrowable.php ├── Process │ └── Entities │ │ └── Process.php ├── Job │ ├── Entities │ │ └── Job.php │ ├── Repositories │ │ └── Job.php │ └── Exceptions │ │ └── JobFailed.php └── Requirement │ └── Entities │ └── Requirement.php ├── src ├── Http │ ├── Controllers │ │ ├── AppAction.php │ │ ├── Api │ │ │ ├── Jobs │ │ │ │ ├── CollectAction.php │ │ │ │ ├── GetAction.php │ │ │ │ ├── DeleteAction.php │ │ │ │ ├── PostRequest.php │ │ │ │ ├── GetResponse.php │ │ │ │ ├── DeleteResponse.php │ │ │ │ ├── PostResponse.php │ │ │ │ ├── CollectResponse.php │ │ │ │ └── PostAction.php │ │ │ ├── Repositories │ │ │ │ ├── CollectResponse.php │ │ │ │ └── CollectAction.php │ │ │ └── Requirements │ │ │ │ ├── CollectResponse.php │ │ │ │ └── CollectAction.php │ │ └── AppResponse.php │ └── Middlewares │ │ └── Authorize.php ├── Job │ ├── Listeners │ │ └── JobRunner.php │ ├── Events │ │ ├── JobHasBeenCreated.php │ │ └── JobHasBeenTerminated.php │ ├── QueueJobs │ │ └── RunJob.php │ ├── Entities │ │ └── Job.php │ └── Repositories │ │ └── JobFileRepository.php ├── Process │ └── Entities │ │ └── Process.php ├── Requirement │ └── Entities │ │ └── Requirement.php ├── Console │ └── Commands │ │ └── Setup.php ├── PaketServiceProvider.php └── Support │ └── Composer.php ├── LICENSE ├── routes └── web.php ├── webpack.mix.js ├── package.json ├── config └── paket.php ├── composer.json ├── CHANGELOG.md └── README.md /public/mix-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "/app.js": "/app.js?id=5240d5562cd611adb118", 3 | "/app.css": "/app.css?id=eba6b5b861d7be75b2c7" 4 | } 5 | -------------------------------------------------------------------------------- /resources/sass/app.scss: -------------------------------------------------------------------------------- 1 | @import '~tailwindcss/base'; 2 | 3 | @import '~tailwindcss/components'; 4 | 5 | @import '~tailwindcss/utilities'; 6 | 7 | @import '~@ibm/plex/scss/ibm-plex'; 8 | 9 | @import 'base'; -------------------------------------------------------------------------------- /tailwind.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | theme: { 3 | extend: {} 4 | }, 5 | variants: {}, 6 | plugins: [], 7 | purge: [ 8 | './resources/js/**/*.vue', 9 | './resources/js/**/**/*.vue', 10 | ], 11 | }; 12 | -------------------------------------------------------------------------------- /contracts/Exceptions/PaketThrowable.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Cog\Contracts\Paket\Exceptions; 15 | 16 | interface PaketThrowable 17 | { 18 | } 19 | -------------------------------------------------------------------------------- /contracts/Process/Entities/Process.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Cog\Contracts\Paket\Process\Entities; 15 | 16 | interface Process 17 | { 18 | public static function fromArray(array $process): self; 19 | 20 | public function toArray(): array; 21 | } 22 | -------------------------------------------------------------------------------- /src/Http/Controllers/AppAction.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Cog\Laravel\Paket\Http\Controllers; 15 | 16 | use Illuminate\Contracts\Support\Responsable as ResponsableContract; 17 | 18 | final class AppAction 19 | { 20 | public function __invoke(): ResponsableContract 21 | { 22 | return new AppResponse(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /resources/sass/base.scss: -------------------------------------------------------------------------------- 1 | body, 2 | .font-mono { 3 | font-family: 'IBM Plex Mono', monospace; 4 | } 5 | 6 | @-webkit-keyframes spin { 7 | 0% { 8 | -webkit-transform: rotate(0deg); 9 | transform: rotate(0deg) 10 | } 11 | to { 12 | -webkit-transform: rotate(1turn); 13 | transform: rotate(1turn) 14 | } 15 | } 16 | 17 | @keyframes spin { 18 | 0% { 19 | -webkit-transform: rotate(0deg); 20 | transform: rotate(0deg) 21 | } 22 | to { 23 | -webkit-transform: rotate(1turn); 24 | transform: rotate(1turn) 25 | } 26 | } 27 | 28 | .spin { 29 | -webkit-animation: spin 2s linear infinite; 30 | animation: spin 2s linear infinite; 31 | } 32 | -------------------------------------------------------------------------------- /src/Job/Listeners/JobRunner.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Cog\Laravel\Paket\Job\Listeners; 15 | 16 | use Cog\Laravel\Paket\Job\Events\JobHasBeenCreated; 17 | use Cog\Laravel\Paket\Job\QueueJobs\RunJob; 18 | use Illuminate\Contracts\Queue\ShouldQueue; 19 | 20 | final class JobRunner implements ShouldQueue 21 | { 22 | public function handle(JobHasBeenCreated $event): void 23 | { 24 | RunJob::dispatch($event->getJob()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Job/Events/JobHasBeenCreated.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Cog\Laravel\Paket\Job\Events; 15 | 16 | use Cog\Contracts\Paket\Job\Entities\Job as JobContract; 17 | 18 | final class JobHasBeenCreated 19 | { 20 | private $job; 21 | 22 | public function __construct(JobContract $job) 23 | { 24 | $this->job = $job; 25 | } 26 | 27 | public function getJob(): JobContract 28 | { 29 | return $this->job; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Job/Events/JobHasBeenTerminated.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Cog\Laravel\Paket\Job\Events; 15 | 16 | use Cog\Contracts\Paket\Job\Entities\Job as JobContract; 17 | 18 | final class JobHasBeenTerminated 19 | { 20 | private $job; 21 | 22 | public function __construct(JobContract $job) 23 | { 24 | $this->job = $job; 25 | } 26 | 27 | public function getJob(): JobContract 28 | { 29 | return $this->job; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /contracts/Job/Entities/Job.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Cog\Contracts\Paket\Job\Entities; 15 | 16 | use Cog\Contracts\Paket\Requirement\Entities\Requirement; 17 | 18 | interface Job 19 | { 20 | public static function fromArray(array $job): self; 21 | 22 | public function toArray(): array; 23 | 24 | public function getType(): string; 25 | 26 | public function getId(): string; 27 | 28 | public function getRequirement(): Requirement; 29 | } 30 | -------------------------------------------------------------------------------- /src/Http/Controllers/Api/Jobs/CollectAction.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Cog\Laravel\Paket\Http\Controllers\Api\Jobs; 15 | 16 | use Cog\Contracts\Paket\Job\Repositories\Job as JobRepositoryContract; 17 | use Illuminate\Contracts\Support\Responsable as ResponsableContract; 18 | 19 | final class CollectAction 20 | { 21 | public function __invoke(JobRepositoryContract $jobs): ResponsableContract 22 | { 23 | return new CollectResponse($jobs->all()); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Http/Controllers/Api/Jobs/GetAction.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Cog\Laravel\Paket\Http\Controllers\Api\Jobs; 15 | 16 | use Cog\Contracts\Paket\Job\Repositories\Job as JobRepositoryContract; 17 | use Illuminate\Contracts\Support\Responsable as ResponsableContract; 18 | 19 | final class GetAction 20 | { 21 | public function __invoke(string $id, JobRepositoryContract $jobs): ResponsableContract 22 | { 23 | return new GetResponse($jobs->getById($id)); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /contracts/Requirement/Entities/Requirement.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Cog\Contracts\Paket\Requirement\Entities; 15 | 16 | interface Requirement 17 | { 18 | public static function fromArray(array $requirement): self; 19 | 20 | public function toArray(): array; 21 | 22 | public function getName(): string; 23 | 24 | public function getVersion(): ?string; 25 | 26 | public function isDevelopment(): bool; 27 | 28 | public function isNotDevelopment(): bool; 29 | } 30 | -------------------------------------------------------------------------------- /contracts/Job/Repositories/Job.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Cog\Contracts\Paket\Job\Repositories; 15 | 16 | use Cog\Contracts\Paket\Job\Entities\Job as JobEntity; 17 | 18 | interface Job 19 | { 20 | public function all(): iterable; 21 | 22 | public function getById(string $id): JobEntity; 23 | 24 | public function store(JobEntity $job): void; 25 | 26 | public function changeJobStatus(JobEntity $job, string $statusName): void; 27 | 28 | public function changeJobExitCode(JobEntity $job, int $exitCode): void; 29 | } 30 | -------------------------------------------------------------------------------- /src/Http/Controllers/Api/Jobs/DeleteAction.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Cog\Laravel\Paket\Http\Controllers\Api\Jobs; 15 | 16 | use Cog\Contracts\Paket\Job\Repositories\Job as JobRepositoryContract; 17 | use Illuminate\Contracts\Support\Responsable as ResponsableContract; 18 | 19 | final class DeleteAction 20 | { 21 | public function __invoke(string $id, JobRepositoryContract $jobs): ResponsableContract 22 | { 23 | $job = $jobs->getById($id); 24 | $jobs->deleteById($id); 25 | 26 | return new DeleteResponse($job); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Http/Middlewares/Authorize.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Cog\Laravel\Paket\Http\Middlewares; 15 | 16 | use Closure; 17 | use Illuminate\Http\Request; 18 | use Symfony\Component\HttpFoundation\Response; 19 | 20 | final class Authorize 21 | { 22 | /** 23 | * Handle the incoming request. 24 | * 25 | * @param \Illuminate\Http\Request $request 26 | * @param \Closure $next 27 | * @return \Symfony\Component\HttpFoundation\Response 28 | */ 29 | public function handle(Request $request, Closure $next): Response 30 | { 31 | return app()->environment('local') ? $next($request) : abort(403); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /resources/js/app.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import VueRouter from 'vue-router'; 3 | 4 | import globals from './globals'; 5 | import routes from './routes'; 6 | import store from './store'; 7 | 8 | Vue.use(VueRouter); 9 | 10 | const router = new VueRouter({ 11 | routes: routes, 12 | mode: 'history', 13 | base: '/' + window.Paket.baseUri, 14 | }); 15 | 16 | Vue.component('top-menu', require('./components/TopMenu.vue').default); 17 | 18 | Vue.mixin(globals); 19 | 20 | new Vue({ 21 | el: '#paket', 22 | 23 | router, 24 | 25 | store, 26 | 27 | created() { 28 | this.fetchData(); 29 | }, 30 | 31 | methods: { 32 | async fetchData() { 33 | // Need it to run jobs state checker after page reload 34 | await this.$store.dispatch('autoRefreshJobs'); 35 | await this.$store.dispatch('collectRequirements'); 36 | await this.$store.dispatch('collectRepositories'); 37 | }, 38 | }, 39 | }); 40 | -------------------------------------------------------------------------------- /resources/views/app.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Paket | {{ config('app.name', 'Laravel') }} 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 |
20 | 21 |
22 |
23 | 24 | 25 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/Http/Controllers/Api/Jobs/PostRequest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Cog\Laravel\Paket\Http\Controllers\Api\Jobs; 15 | 16 | use Illuminate\Foundation\Http\FormRequest; 17 | 18 | final class PostRequest extends FormRequest 19 | { 20 | public function rules(): array 21 | { 22 | return [ 23 | 'requirement.name' => [ 24 | 'required', 25 | 'string', 26 | ], 27 | 'requirement.version' => [ 28 | 'nullable', 29 | 'string', 30 | ], 31 | 'requirement.isDevelopment' => [ 32 | 'nullable', 33 | 'boolean', 34 | ], 35 | ]; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /resources/js/routes.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | path: '/', 4 | redirect: '/dashboard', 5 | }, 6 | 7 | { 8 | path: '/dashboard', 9 | name: 'dashboard', 10 | component: require('./screens/dashboard/index').default, 11 | }, 12 | 13 | { 14 | path: '/laravel', 15 | name: 'laravel', 16 | component: require('./screens/laravel/index').default, 17 | }, 18 | 19 | { 20 | path: '/requirements', 21 | name: 'requirements', 22 | component: require('./screens/requirements/index').default, 23 | }, 24 | 25 | { 26 | path: '/repositories', 27 | name: 'repositories', 28 | component: require('./screens/repositories/index').default, 29 | }, 30 | 31 | { 32 | path: '/jobs', 33 | name: 'jobs', 34 | component: require('./screens/jobs/index').default, 35 | }, 36 | 37 | { 38 | path: '/jobs/:id', 39 | name: 'job', 40 | component: require('./screens/jobs/item').default, 41 | }, 42 | ]; 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020, Anton Komarev 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 | -------------------------------------------------------------------------------- /src/Process/Entities/Process.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Cog\Laravel\Paket\Process\Entities; 15 | 16 | use Cog\Contracts\Paket\Process\Entities\Process as ProcessContract; 17 | 18 | final class Process implements ProcessContract 19 | { 20 | private $output; 21 | 22 | private $exitCode; 23 | 24 | public function __construct(?string $output = null, ?int $exitCode = null) 25 | { 26 | $this->output = $output; 27 | $this->exitCode = $exitCode; 28 | } 29 | 30 | public static function fromArray(array $process): ProcessContract 31 | { 32 | return new self( 33 | $process['output'] ?? null, 34 | $process['exitCode'] ?? null 35 | ); 36 | } 37 | 38 | public function toArray(): array 39 | { 40 | return [ 41 | 'output' => $this->output, 42 | 'exitCode' => $this->exitCode, 43 | ]; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Http/Controllers/Api/Repositories/CollectResponse.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Cog\Laravel\Paket\Http\Controllers\Api\Repositories; 15 | 16 | use Illuminate\Contracts\Support\Responsable as ResponsableContract; 17 | use Illuminate\Http\JsonResponse; 18 | 19 | final class CollectResponse implements ResponsableContract 20 | { 21 | private $repositories; 22 | 23 | public function __construct(iterable $repositories) 24 | { 25 | $this->repositories = $repositories; 26 | } 27 | 28 | /** 29 | * Create an HTTP response that represents the object. 30 | * 31 | * @param \Illuminate\Http\Request $request 32 | * @return \Symfony\Component\HttpFoundation\Response 33 | */ 34 | public function toResponse($request) 35 | { 36 | return $this->toJson(); 37 | } 38 | 39 | private function toJson(): JsonResponse 40 | { 41 | return response()->json($this->repositories); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Http/Controllers/Api/Requirements/CollectResponse.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Cog\Laravel\Paket\Http\Controllers\Api\Requirements; 15 | 16 | use Illuminate\Contracts\Support\Responsable as ResponsableContract; 17 | use Illuminate\Http\JsonResponse; 18 | 19 | final class CollectResponse implements ResponsableContract 20 | { 21 | private $requirements; 22 | 23 | public function __construct(iterable $requirements) 24 | { 25 | $this->requirements = $requirements; 26 | } 27 | 28 | /** 29 | * Create an HTTP response that represents the object. 30 | * 31 | * @param \Illuminate\Http\Request $request 32 | * @return \Symfony\Component\HttpFoundation\Response 33 | */ 34 | public function toResponse($request) 35 | { 36 | return $this->toJson(); 37 | } 38 | 39 | private function toJson(): JsonResponse 40 | { 41 | return response()->json($this->requirements); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Http/Controllers/Api/Jobs/GetResponse.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Cog\Laravel\Paket\Http\Controllers\Api\Jobs; 15 | 16 | use Cog\Contracts\Paket\Job\Entities\Job as JobContract; 17 | use Illuminate\Contracts\Support\Responsable as ResponsableContract; 18 | use Illuminate\Http\JsonResponse; 19 | 20 | final class GetResponse implements ResponsableContract 21 | { 22 | private $job; 23 | 24 | public function __construct(JobContract $job) 25 | { 26 | $this->job = $job; 27 | } 28 | 29 | /** 30 | * Create an HTTP response that represents the object. 31 | * 32 | * @param \Illuminate\Http\Request $request 33 | * @return \Symfony\Component\HttpFoundation\Response 34 | */ 35 | public function toResponse($request) 36 | { 37 | return $this->toJson(); 38 | } 39 | 40 | private function toJson(): JsonResponse 41 | { 42 | return response()->json($this->job->toArray()); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Http/Controllers/Api/Jobs/DeleteResponse.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Cog\Laravel\Paket\Http\Controllers\Api\Jobs; 15 | 16 | use Cog\Contracts\Paket\Job\Entities\Job as JobContract; 17 | use Illuminate\Contracts\Support\Responsable as ResponsableContract; 18 | use Illuminate\Http\JsonResponse; 19 | 20 | final class DeleteResponse implements ResponsableContract 21 | { 22 | private $job; 23 | 24 | public function __construct(JobContract $job) 25 | { 26 | $this->job = $job; 27 | } 28 | 29 | /** 30 | * Create an HTTP response that represents the object. 31 | * 32 | * @param \Illuminate\Http\Request $request 33 | * @return \Symfony\Component\HttpFoundation\Response 34 | */ 35 | public function toResponse($request) 36 | { 37 | return $this->toJson(); 38 | } 39 | 40 | private function toJson(): JsonResponse 41 | { 42 | return response()->json($this->job->toArray()); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Http/Controllers/Api/Jobs/PostResponse.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Cog\Laravel\Paket\Http\Controllers\Api\Jobs; 15 | 16 | use Cog\Contracts\Paket\Job\Entities\Job as JobContract; 17 | use Illuminate\Contracts\Support\Responsable as ResponsableContract; 18 | use Illuminate\Http\JsonResponse; 19 | 20 | final class PostResponse implements ResponsableContract 21 | { 22 | private $job; 23 | 24 | public function __construct(JobContract $job) 25 | { 26 | $this->job = $job; 27 | } 28 | 29 | /** 30 | * Create an HTTP response that represents the object. 31 | * 32 | * @param \Illuminate\Http\Request $request 33 | * @return \Symfony\Component\HttpFoundation\Response 34 | */ 35 | public function toResponse($request) 36 | { 37 | return $this->toJson(); 38 | } 39 | 40 | private function toJson(): JsonResponse 41 | { 42 | return response()->json($this->job->toArray(), 201); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /routes/web.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | use Illuminate\Support\Facades\Route; 15 | 16 | Route::prefix('api')->namespace('Api')->name('paket.api.')->group(function () { 17 | Route::get('repositories') 18 | ->uses('Repositories\CollectAction') 19 | ->name('repositories.collect'); 20 | 21 | Route::get('requirements') 22 | ->uses('Requirements\CollectAction') 23 | ->name('requirements.collect'); 24 | 25 | Route::get('jobs') 26 | ->uses('Jobs\CollectAction') 27 | ->name('jobs.collect'); 28 | 29 | Route::post('jobs') 30 | ->uses('Jobs\PostAction') 31 | ->name('jobs.post'); 32 | 33 | Route::get('jobs/{job}') 34 | ->uses('Jobs\GetAction') 35 | ->name('jobs.get'); 36 | 37 | Route::delete('jobs/{job}') 38 | ->uses('Jobs\DeleteAction') 39 | ->name('jobs.delete'); 40 | }); 41 | 42 | Route::get('{view?}', 'AppAction') 43 | ->where('view', '(.*)') 44 | ->name('paket'); 45 | -------------------------------------------------------------------------------- /resources/js/screens/repositories/index.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 39 | -------------------------------------------------------------------------------- /src/Http/Controllers/AppResponse.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Cog\Laravel\Paket\Http\Controllers; 15 | 16 | use Illuminate\Contracts\Support\Responsable as ResponsableContract; 17 | use Illuminate\Http\Response as IlluminateResponse; 18 | use Illuminate\Support\Facades\Config; 19 | 20 | final class AppResponse implements ResponsableContract 21 | { 22 | /** 23 | * Create an HTTP response that represents the object. 24 | * 25 | * @param \Illuminate\Http\Request $request 26 | * @return \Symfony\Component\HttpFoundation\Response 27 | */ 28 | public function toResponse($request) 29 | { 30 | return $this->toHtml(); 31 | } 32 | 33 | private function toHtml(): IlluminateResponse 34 | { 35 | return response()->view('paket::app', [ 36 | 'cssFile' => 'app.css', 37 | 'paketScriptVariables' => [ 38 | 'baseUri' => ltrim(Config::get('paket.base_uri', 'paket'), '/'), 39 | ], 40 | ]); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Http/Controllers/Api/Jobs/CollectResponse.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Cog\Laravel\Paket\Http\Controllers\Api\Jobs; 15 | 16 | use Illuminate\Contracts\Support\Responsable as ResponsableContract; 17 | use Illuminate\Http\JsonResponse; 18 | 19 | final class CollectResponse implements ResponsableContract 20 | { 21 | private $jobs; 22 | 23 | public function __construct(iterable $jobs) 24 | { 25 | $this->jobs = $jobs; 26 | } 27 | 28 | /** 29 | * Create an HTTP response that represents the object. 30 | * 31 | * @param \Illuminate\Http\Request $request 32 | * @return \Symfony\Component\HttpFoundation\Response 33 | */ 34 | public function toResponse($request) 35 | { 36 | return $this->toJson(); 37 | } 38 | 39 | private function toJson(): JsonResponse 40 | { 41 | $jobs = []; 42 | foreach ($this->jobs as $job) { 43 | $jobs[] = $job->toArray(); 44 | } 45 | 46 | return response()->json($jobs); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /resources/js/components/Job/StatusBadge.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /webpack.mix.js: -------------------------------------------------------------------------------- 1 | const mix = require('laravel-mix'); 2 | const tailwindcss = require('tailwindcss'); 3 | const webpack = require('webpack'); 4 | 5 | /* 6 | |-------------------------------------------------------------------------- 7 | | Mix Asset Management 8 | |-------------------------------------------------------------------------- 9 | | 10 | | Mix provides a clean, fluent API for defining some Webpack build steps 11 | | for your Laravel application. By default, we are compiling the Sass 12 | | file for the application as well as bundling up all the JS files. 13 | | 14 | */ 15 | 16 | mix.options({ 17 | uglify: { 18 | uglifyOptions: { 19 | compress: { 20 | drop_console: true, 21 | }, 22 | }, 23 | }, 24 | processCssUrls: false, 25 | postCss: [tailwindcss('./tailwind.js')], 26 | }) 27 | .setPublicPath('public') 28 | .js('resources/js/app.js', 'public') 29 | .sass('resources/sass/app.scss', 'public') 30 | .version() 31 | .copy('public', '../sandbox-laravel-paket/public/vendor/paket') 32 | .webpackConfig({ 33 | resolve: { 34 | symlinks: false, 35 | alias: { 36 | '@': path.resolve(__dirname, 'resources/js/'), 37 | }, 38 | }, 39 | plugins: [new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/)], 40 | }); 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "dev": "npm run development", 5 | "development": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js", 6 | "watch": "npm run development -- --watch", 7 | "watch-poll": "npm run watch -- --watch-poll", 8 | "hot": "cross-env NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot --config=node_modules/laravel-mix/setup/webpack.config.js", 9 | "prod": "npm run production", 10 | "production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --no-progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js" 11 | }, 12 | "devDependencies": { 13 | "@ibm/plex": "^2.0.0", 14 | "ansi-to-html": "^0.6.10", 15 | "axios": "^0.21", 16 | "cross-env": "^5.1", 17 | "laravel-mix": "^4.0.7", 18 | "moment": "^2.29.4", 19 | "moment-timezone": "^0.5.35", 20 | "resolve-url-loader": "^2.3.1", 21 | "sass": "^1.15.2", 22 | "sass-loader": "^7.1.0", 23 | "sweetalert2": "^8.13.0", 24 | "tailwindcss": "^1.1.2", 25 | "vue": "^2.5.7", 26 | "vue-router": "^3.0.1", 27 | "vue-template-compiler": "^2.5.21", 28 | "vuex": "^3.1.1" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /contracts/Job/Exceptions/JobFailed.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Cog\Contracts\Paket\Job\Exceptions; 15 | 16 | use Cog\Contracts\Paket\Exceptions\PaketThrowable; 17 | use Cog\Contracts\Paket\Job\Entities\Job as JobContract; 18 | use RuntimeException; 19 | use Throwable; 20 | 21 | final class JobFailed extends RuntimeException implements 22 | PaketThrowable 23 | { 24 | private $job; 25 | 26 | private $exitCode = 0; 27 | 28 | public function __construct( 29 | JobContract $job, 30 | int $exitCode, 31 | string $message = '', 32 | int $code = 0, 33 | Throwable $previous = null 34 | ) { 35 | parent::__construct($message, $code, $previous); 36 | 37 | $this->job = $job; 38 | $this->exitCode = $exitCode; 39 | } 40 | 41 | public static function withExitCode(JobContract $job, int $code): self 42 | { 43 | return new static($job, $code, sprintf('Job `%s` failed. Exit code: %d', $job->getId(), $code)); 44 | } 45 | 46 | public function getJob(): JobContract 47 | { 48 | return $this->job; 49 | } 50 | 51 | public function getExitCode(): int 52 | { 53 | return $this->exitCode; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Http/Controllers/Api/Repositories/CollectAction.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Cog\Laravel\Paket\Http\Controllers\Api\Repositories; 15 | 16 | use Illuminate\Contracts\Support\Responsable as ResponsableContract; 17 | use MCStreetguy\ComposerParser\Factory as ComposerParser; 18 | 19 | final class CollectAction 20 | { 21 | public function __invoke(): ResponsableContract 22 | { 23 | $repositories = $this->getRepositories(); 24 | 25 | return new CollectResponse($repositories); 26 | } 27 | 28 | private function getRepositories(): array 29 | { 30 | $jsonFile = ComposerParser::parseComposerJson(base_path('composer.json')); 31 | 32 | $repositories = $jsonFile->getRepositories(); 33 | $output = []; 34 | 35 | foreach ($repositories as $key => $repository) { 36 | $package = $repository->getPackage(); 37 | 38 | $output[$key] = [ 39 | 'type' => $repository->getType(), 40 | 'url' => $repository->getUrl(), 41 | 'options' => $repository->getOptions(), 42 | 'package' => is_null($package) ? null : $package->toArray(), 43 | ]; 44 | } 45 | 46 | return $output; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /config/paket.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | use Cog\Laravel\Paket\Http\Middlewares\Authorize; 15 | 16 | return [ 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Paket Base URI 21 | |-------------------------------------------------------------------------- 22 | | 23 | | This is the URI path where Paket will be accessible from. Feel free 24 | | to change this path to anything you like. 25 | | 26 | */ 27 | 28 | 'base_uri' => env('PAKET_BASE_URI', 'paket'), 29 | 30 | /* 31 | |-------------------------------------------------------------------------- 32 | | Composer PATH 33 | |-------------------------------------------------------------------------- 34 | | 35 | | Composer executable file location. 36 | | 37 | */ 38 | 39 | 'composer_path' => env('PAKET_COMPOSER_PATH', '/usr/bin/composer'), 40 | 41 | /* 42 | |-------------------------------------------------------------------------- 43 | | Paket Route Middlewares 44 | |-------------------------------------------------------------------------- 45 | | 46 | | These middlewares will be assigned to every Paket route, giving you 47 | | the chance to add your own middleware to this list or change any of 48 | | the existing middlewares. Or, you can simply stick with this list. 49 | | 50 | */ 51 | 52 | 'middlewares' => [ 53 | 'web', 54 | Authorize::class, 55 | ], 56 | 57 | ]; 58 | -------------------------------------------------------------------------------- /resources/js/globals.js: -------------------------------------------------------------------------------- 1 | import Swal from 'sweetalert2'; 2 | 3 | export default { 4 | computed: { 5 | Paket() { 6 | return window.Paket; 7 | }, 8 | }, 9 | 10 | methods: { 11 | alertSuccess: async (data) => { 12 | await Swal.fire({ 13 | type: 'success', 14 | ...data, 15 | }); 16 | }, 17 | 18 | alertInfo: async (data) => { 19 | await Swal.fire({ 20 | type: 'info', 21 | ...data, 22 | }); 23 | }, 24 | 25 | alertWarning: async (data) => { 26 | await Swal.fire({ 27 | type: 'warning', 28 | ...data, 29 | }); 30 | }, 31 | 32 | alertError: async (exception) => { 33 | let errorData = {}; 34 | 35 | if (exception.response.status === 422) { 36 | let error; 37 | 38 | for (let errorKey in exception.response.data.errors) { 39 | if (exception.response.data.errors.hasOwnProperty(errorKey)) { 40 | error = exception.response.data.errors[errorKey][0]; 41 | break; 42 | } 43 | } 44 | 45 | errorData = { 46 | type: 'error', 47 | title: 'Validation Error!', 48 | text: error, 49 | }; 50 | } else { 51 | errorData = { 52 | type: 'error', 53 | title: 'Error!', 54 | text: exception.message, 55 | }; 56 | } 57 | 58 | await Swal.fire(errorData); 59 | }, 60 | }, 61 | } 62 | -------------------------------------------------------------------------------- /src/Requirement/Entities/Requirement.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Cog\Laravel\Paket\Requirement\Entities; 15 | 16 | use Cog\Contracts\Paket\Requirement\Entities\Requirement as RequirementContract; 17 | 18 | final class Requirement implements RequirementContract 19 | { 20 | private $name; 21 | 22 | private $version; 23 | 24 | private $isDevelopment; 25 | 26 | public function __construct(string $name, ?string $version = null, bool $isDevelopment = false) 27 | { 28 | $this->name = $name; 29 | $this->version = $version; 30 | $this->isDevelopment = $isDevelopment; 31 | } 32 | 33 | public function __toString(): string 34 | { 35 | if (is_null($this->version)) { 36 | return $this->name; 37 | } 38 | 39 | return sprintf('%s:%s', $this->name, $this->version); 40 | } 41 | 42 | public static function fromArray(array $requirement): RequirementContract 43 | { 44 | return new self( 45 | $requirement['name'], 46 | $requirement['version'] ?? null, 47 | $requirement['isDevelopment'] ?? false 48 | ); 49 | } 50 | 51 | public function toArray(): array 52 | { 53 | return [ 54 | 'name' => $this->name, 55 | 'version' => $this->version, 56 | 'isDevelopment' => $this->isDevelopment, 57 | ]; 58 | } 59 | 60 | public function getName(): string 61 | { 62 | return $this->name; 63 | } 64 | 65 | public function getVersion(): ?string 66 | { 67 | return $this->version; 68 | } 69 | 70 | public function isDevelopment(): bool 71 | { 72 | return $this->isDevelopment; 73 | } 74 | 75 | public function isNotDevelopment(): bool 76 | { 77 | return !$this->isDevelopment(); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /resources/js/components/Requirement/Requirement.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 66 | -------------------------------------------------------------------------------- /resources/js/components/Requirement/InstallButton.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 61 | -------------------------------------------------------------------------------- /resources/js/components/Requirement/UninstallButton.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 61 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cybercog/laravel-paket", 3 | "description": "Composer personal web interface. Manage Laravel dependencies without switching to command line!", 4 | "type": "library", 5 | "license": "MIT", 6 | "keywords": [ 7 | "cybercog", 8 | "cog", 9 | "laravel", 10 | "composer", 11 | "packagist", 12 | "package", 13 | "manager", 14 | "management", 15 | "repository", 16 | "packet", 17 | "paket", 18 | "dependency", 19 | "artifact", 20 | "library", 21 | "development", 22 | "gui" 23 | ], 24 | "authors": [ 25 | { 26 | "name": "Anton Komarev", 27 | "email": "anton@komarev.com", 28 | "homepage": "https://komarev.com", 29 | "role": "Developer" 30 | } 31 | ], 32 | "homepage": "https://komarev.com/sources/laravel-paket", 33 | "support": { 34 | "email": "open@cybercog.su", 35 | "issues": "https://github.com/cybercog/laravel-paket/issues", 36 | "wiki": "https://github.com/cybercog/laravel-paket/wiki", 37 | "source": "https://github.com/cybercog/laravel-paket", 38 | "docs": "https://laravel-paket.readme.io" 39 | }, 40 | "require": { 41 | "php": "^7.1.3 | ^8.0", 42 | "ext-json": "*", 43 | "illuminate/support": "^5.6|^6.0|^7.0|^8.0", 44 | "mcstreetguy/composer-parser": "^1.1" 45 | }, 46 | "require-dev": { 47 | "mockery/mockery": "^1.0", 48 | "orchestra/testbench": "^3.6|^4.0|^5.0|^6.0", 49 | "phpunit/phpunit": "^7.5|^8.0|^9.0" 50 | }, 51 | "autoload": { 52 | "psr-4": { 53 | "Cog\\Contracts\\Paket\\": "contracts/", 54 | "Cog\\Laravel\\Paket\\": "src/" 55 | } 56 | }, 57 | "autoload-dev": { 58 | "psr-4": { 59 | "Cog\\Tests\\Laravel\\Paket\\": "tests/" 60 | } 61 | }, 62 | "scripts": { 63 | "test": "vendor/bin/phpunit" 64 | }, 65 | "config": { 66 | "sort-packages": true 67 | }, 68 | "extra": { 69 | "laravel": { 70 | "providers": [ 71 | "Cog\\Laravel\\Paket\\PaketServiceProvider" 72 | ] 73 | } 74 | }, 75 | "minimum-stability": "dev", 76 | "prefer-stable" : true 77 | } 78 | -------------------------------------------------------------------------------- /src/Job/QueueJobs/RunJob.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Cog\Laravel\Paket\Job\QueueJobs; 15 | 16 | use Cog\Contracts\Paket\Job\Entities\Job as JobContract; 17 | use Cog\Contracts\Paket\Job\Exceptions\JobFailed; 18 | use Cog\Contracts\Paket\Job\Repositories\Job as JobRepositoryContract; 19 | use Cog\Laravel\Paket\Job\Events\JobHasBeenTerminated; 20 | use Cog\Laravel\Paket\Support\Composer; 21 | use Illuminate\Bus\Queueable; 22 | use Illuminate\Contracts\Queue\ShouldQueue; 23 | use Illuminate\Foundation\Bus\Dispatchable; 24 | use Illuminate\Queue\InteractsWithQueue; 25 | use RuntimeException; 26 | 27 | final class RunJob implements ShouldQueue 28 | { 29 | use Dispatchable; 30 | use InteractsWithQueue; 31 | use Queueable; 32 | 33 | private $paketJob; 34 | 35 | public function __construct(JobContract $paketJob) 36 | { 37 | $this->paketJob = $paketJob; 38 | } 39 | 40 | public function handle(JobRepositoryContract $jobs, Composer $composer): void 41 | { 42 | $jobs->changeJobStatus($this->paketJob, 'Running'); 43 | 44 | try { 45 | switch ($this->paketJob->getType()) { 46 | case 'RequirementInstall': 47 | $composer->install($this->paketJob->getRequirement(), $this->paketJob); 48 | break; 49 | case 'RequirementUninstall': 50 | $composer->uninstall($this->paketJob->getRequirement(), $this->paketJob); 51 | break; 52 | default: 53 | // TODO: Throw custom exception 54 | throw new RuntimeException('Unknown type of job'); 55 | } 56 | 57 | $jobs->changeJobStatus($this->paketJob, 'Success'); 58 | $jobs->changeJobExitCode($this->paketJob, 0); 59 | } catch (JobFailed $exception) { 60 | $jobs->changeJobStatus($this->paketJob, 'Failed'); 61 | $jobs->changeJobExitCode($this->paketJob, $exception->getExitCode()); 62 | } 63 | 64 | event(new JobHasBeenTerminated($this->paketJob)); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Job/Entities/Job.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Cog\Laravel\Paket\Job\Entities; 15 | 16 | use Cog\Contracts\Paket\Job\Entities\Job as JobContract; 17 | use Cog\Contracts\Paket\Process\Entities\Process as ProcessContract; 18 | use Cog\Contracts\Paket\Requirement\Entities\Requirement as RequirementContract; 19 | use Cog\Laravel\Paket\Process\Entities\Process; 20 | use Cog\Laravel\Paket\Requirement\Entities\Requirement; 21 | use DateTimeInterface; 22 | use Illuminate\Support\Carbon; 23 | 24 | final class Job implements JobContract 25 | { 26 | private $type; 27 | 28 | private $id; 29 | 30 | private $status; 31 | 32 | private $process; 33 | 34 | private $requirement; 35 | 36 | private $createdAt; 37 | 38 | public function __construct( 39 | string $type, 40 | string $id, 41 | string $status, 42 | DateTimeInterface $createdAt, 43 | ProcessContract $process, 44 | ?RequirementContract $requirement = null 45 | ) { 46 | $this->type = $type; 47 | $this->id = $id; 48 | $this->status = $status; 49 | $this->createdAt = $createdAt; 50 | $this->process = $process; 51 | $this->requirement = $requirement; 52 | } 53 | 54 | public static function fromArray(array $job): JobContract 55 | { 56 | return new self( 57 | $job['type'], 58 | $job['id'], 59 | $job['status'], 60 | Carbon::createFromFormat(DATE_RFC3339_EXTENDED, $job['createdAt']), 61 | Process::fromArray($job['process']), 62 | Requirement::fromArray($job['requirement']) 63 | ); 64 | } 65 | 66 | public function toArray(): array 67 | { 68 | return [ 69 | 'type' => $this->getType(), 70 | 'id' => $this->getId(), 71 | 'status' => $this->status, 72 | 'requirement' => $this->requirement->toArray(), 73 | 'process' => $this->process->toArray(), 74 | 'createdAt' => $this->createdAt->format(DATE_RFC3339_EXTENDED), 75 | ]; 76 | } 77 | 78 | public function getType(): string 79 | { 80 | return $this->type; 81 | } 82 | 83 | public function getId(): string 84 | { 85 | return $this->id; 86 | } 87 | 88 | public function getRequirement(): RequirementContract 89 | { 90 | return $this->requirement; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /resources/js/components/Job/OptionsMenu.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | -------------------------------------------------------------------------------- /resources/js/components/Repository/Repository.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 79 | -------------------------------------------------------------------------------- /src/Console/Commands/Setup.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Cog\Laravel\Paket\Console\Commands; 15 | 16 | use Illuminate\Console\Command; 17 | use Illuminate\Support\Facades\File; 18 | use Symfony\Component\Console\Input\InputOption; 19 | 20 | final class Setup extends Command 21 | { 22 | protected $name = 'paket:setup'; 23 | 24 | protected $description = 'Set up all of the Paket resources'; 25 | 26 | public function handle(): int 27 | { 28 | $this->warn('Paket scaffolding set up starting'); 29 | $this->publishAssets(); 30 | $this->createPaketStorage(storage_path('paket')); 31 | $this->createPaketJobsStorage(storage_path('paket/jobs')); 32 | $this->info('Paket scaffolding set up completed'); 33 | 34 | return 0; 35 | } 36 | 37 | protected function getOptions(): array 38 | { 39 | return [ 40 | ['force', null, InputOption::VALUE_NONE, 'Overwrite any existing files'], 41 | ]; 42 | } 43 | 44 | private function publishAssets(): void 45 | { 46 | if ($this->option('force')) { 47 | $this->warn('Force publishing Paket assets'); 48 | $this->call('vendor:publish', [ 49 | '--tag' => 'paket-assets', 50 | '--force' => $this->option('force'), 51 | ]); 52 | } else { 53 | $this->warn('Publishing Paket assets'); 54 | $this->call('vendor:publish', [ 55 | '--tag' => 'paket-assets', 56 | ]); 57 | } 58 | $this->info('Published Paket assets'); 59 | } 60 | 61 | private function createPaketStorage(string $path): void 62 | { 63 | $this->createDirectory($path); 64 | $this->createGitIgnore($path, "*\n!jobs/\n!.gitignore\n"); 65 | } 66 | 67 | private function createPaketJobsStorage(string $path): void 68 | { 69 | $this->createDirectory($path); 70 | $this->createGitIgnore($path, "*\n!.gitignore\n"); 71 | } 72 | 73 | private function createDirectory(string $path): void 74 | { 75 | if (file_exists($path)) { 76 | return; 77 | } 78 | 79 | $this->warn("Creating `{$path}` directory"); 80 | mkdir($path); 81 | $this->info("Created `{$path}` directory"); 82 | } 83 | 84 | private function createGitIgnore(string $path, string $content): void 85 | { 86 | $filepath = sprintf('%s/.gitignore', $path); 87 | if (file_exists($filepath)) { 88 | return; 89 | } 90 | 91 | $this->warn("Creating `{$filepath}` file"); 92 | File::put($filepath, $content); 93 | $this->info("Created `{$filepath}` file"); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Http/Controllers/Api/Requirements/CollectAction.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Cog\Laravel\Paket\Http\Controllers\Api\Requirements; 15 | 16 | use Illuminate\Contracts\Support\Responsable as ResponsableContract; 17 | use MCStreetguy\ComposerParser\Factory as ComposerParser; 18 | 19 | final class CollectAction 20 | { 21 | public function __invoke(): ResponsableContract 22 | { 23 | $requirements = $this->getInstalledRequirements(); 24 | 25 | return new CollectResponse($requirements); 26 | } 27 | 28 | private function getInstalledRequirements(): array 29 | { 30 | $lockFile = ComposerParser::parseLockfile(base_path('composer.lock')); 31 | $jsonFile = ComposerParser::parseComposerJson(base_path('composer.json')); 32 | 33 | $packages = $lockFile->getPackages()->getData(); 34 | $devPackages = $lockFile->getPackagesDev()->getData(); 35 | $platform = $lockFile->getPlatform()->getData(); 36 | $devPlatform = $lockFile->getPlatformDev()->getData(); 37 | 38 | foreach ($packages as &$package) { 39 | $package['isDevelopment'] = false; 40 | } 41 | foreach ($devPackages as &$package) { 42 | $package['isDevelopment'] = true; 43 | } 44 | 45 | $requires = $jsonFile->getRequire()->getData(); 46 | $devRequires = $jsonFile->getRequireDev()->getData(); 47 | 48 | $roots = [ 49 | 'essential' => [], 50 | 'dev' => [], 51 | ]; 52 | foreach ($platform as $name => $version) { 53 | $roots['essential'][] = [ 54 | 'name' => $name, 55 | 'version' => $version, 56 | ]; 57 | } 58 | foreach ($devPlatform as $name => $version) { 59 | $roots['dev'][] = [ 60 | 'name' => $name, 61 | 'version' => $version, 62 | ]; 63 | } 64 | foreach ($packages as $key => $package) { 65 | if (isset($requires[$package['name']]) || isset($devRequires[$package['name']])) { 66 | $roots['essential'][] = $package; 67 | unset($packages[$key]); 68 | } 69 | } 70 | foreach ($devPackages as $key => $package) { 71 | if (isset($requires[$package['name']]) || isset($devRequires[$package['name']])) { 72 | $roots['dev'][] = $package; 73 | unset($devPackages[$key]); 74 | } 75 | } 76 | $dependencies = [ 77 | 'essential' => [], 78 | 'dev' => [], 79 | ]; 80 | foreach ($packages as $key => $package) { 81 | $dependencies['essential'][] = $package; 82 | } 83 | foreach ($devPackages as $key => $package) { 84 | $dependencies['dev'][] = $package; 85 | } 86 | 87 | return [ 88 | 'roots' => $roots, 89 | 'dependencies' => $dependencies, 90 | ]; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `cybercog/laravel-paket` will be documented in this file. 4 | 5 | ## [Unreleased] 6 | 7 | ## [1.8.0] 8 | 9 | ### Added 10 | 11 | - ([#75]) Add configurable Composer path 12 | 13 | ## [1.7.0] 14 | 15 | ### Added 16 | 17 | - ([#71]) Add Laravel 8 support 18 | 19 | ## [1.6.0] 20 | 21 | ### Added 22 | 23 | - ([#68]) Add Laravel Sanctum to Laravel Ecosystem tab 24 | - ([#68]) Add PurgeCSS 25 | 26 | ## [1.5.2] 27 | 28 | ### Added 29 | 30 | - ([#64]) Add Laravel 7 support 31 | 32 | ## [1.5.1] 33 | 34 | ### Fixed 35 | 36 | - ([#58]) Fix fallback Base URI value 37 | - ([#61]) Fixed [GHSA-h9rv-jmmf-4pgx](https://github.com/advisories/GHSA-h9rv-jmmf-4pgx) 38 | 39 | ## [1.5.0] 40 | 41 | ### Added 42 | 43 | - ([#48]) Added repositories tab 44 | - ([#51]) Added refetch data button in top menu 45 | - ([#52]) Added polling to get realtime job output 46 | - ([#53]) Added requirement suggestions from Packagist.org 47 | - ([#55]) Trim first slash in .env PAKET_BASE_URI 48 | 49 | ## [1.4.0] 50 | 51 | ### Added 52 | 53 | - ([#39]) Added jobs deletion 54 | - ([#47]) Added job status badge in top menu 55 | - ([#42]) Lock installer when Job in progress 56 | - ([#47]) Unlock installer when Job in progress 57 | 58 | ## [1.3.0] 59 | 60 | ### Added 61 | 62 | - ([#35]) Tab with Laravel Packages 63 | 64 | ## [1.2.0] 65 | 66 | ### Changed 67 | 68 | - ([#33]) UI rebuilt with Tailwind CSS 69 | 70 | ## [1.1.0] - 2019-09-04 71 | 72 | ### Added 73 | 74 | - ([#32]) Laravel v6 support 75 | 76 | ### Changed 77 | 78 | - ([#27]) `Http\Controllers` namespace structure became more flat 79 | 80 | ### Fixed 81 | 82 | - ([#28]) Terminal jobs new line breaks 83 | 84 | ### Security 85 | 86 | - Upgraded `lodash` 87 | 88 | ## 1.0.0 - 2019-06-30 89 | 90 | - Initial release 91 | 92 | [Unreleased]: https://github.com/cybercog/laravel-paket/compare/1.8.0...master 93 | [1.8.0]: https://github.com/cybercog/laravel-paket/compare/1.7.0...1.8.0 94 | [1.7.0]: https://github.com/cybercog/laravel-paket/compare/1.6.0...1.7.0 95 | [1.6.0]: https://github.com/cybercog/laravel-paket/compare/1.5.2...1.6.0 96 | [1.5.2]: https://github.com/cybercog/laravel-paket/compare/1.5.1...1.5.2 97 | [1.5.1]: https://github.com/cybercog/laravel-paket/compare/1.5.0...1.5.1 98 | [1.5.0]: https://github.com/cybercog/laravel-paket/compare/1.4.0...1.5.0 99 | [1.4.0]: https://github.com/cybercog/laravel-paket/compare/1.3.0...1.4.0 100 | [1.3.0]: https://github.com/cybercog/laravel-paket/compare/1.2.0...1.3.0 101 | [1.2.0]: https://github.com/cybercog/laravel-paket/compare/1.1.0...1.2.0 102 | [1.1.0]: https://github.com/cybercog/laravel-paket/compare/1.0.0...1.1.0 103 | 104 | [#75]: https://github.com/cybercog/laravel-paket/pull/75 105 | [#71]: https://github.com/cybercog/laravel-paket/pull/71 106 | [#68]: https://github.com/cybercog/laravel-paket/pull/68 107 | [#64]: https://github.com/cybercog/laravel-paket/pull/64 108 | [#61]: https://github.com/cybercog/laravel-paket/pull/61 109 | [#58]: https://github.com/cybercog/laravel-paket/pull/58 110 | [#55]: https://github.com/cybercog/laravel-paket/pull/55 111 | [#53]: https://github.com/cybercog/laravel-paket/pull/53 112 | [#52]: https://github.com/cybercog/laravel-paket/pull/52 113 | [#51]: https://github.com/cybercog/laravel-paket/pull/51 114 | [#48]: https://github.com/cybercog/laravel-paket/pull/48 115 | [#47]: https://github.com/cybercog/laravel-paket/pull/47 116 | [#42]: https://github.com/cybercog/laravel-paket/pull/42 117 | [#39]: https://github.com/cybercog/laravel-paket/pull/39 118 | [#35]: https://github.com/cybercog/laravel-paket/pull/35 119 | [#33]: https://github.com/cybercog/laravel-paket/pull/33 120 | [#32]: https://github.com/cybercog/laravel-paket/pull/32 121 | [#28]: https://github.com/cybercog/laravel-paket/pull/28 122 | [#27]: https://github.com/cybercog/laravel-paket/pull/27 123 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel Paket 2 | 3 | ![cog-laravel-paket](https://user-images.githubusercontent.com/1849174/67785136-d7ce7a80-fa7d-11e9-8217-eb1c8e0d4d7f.png) 4 | 5 |

6 | Releases 7 | Build Status 8 | StyleCI 9 | Code Quality 10 | License 11 |

12 | 13 | ## Introduction 14 | 15 | Laravel Paket satisfies application requirements. Manage Laravel dependencies without switching to command line! 16 | 17 | Graphical User Interface made with [Tailwind CSS]. 18 | 19 | ## Official Documentation 20 | 21 | Documentation can be found in [Laravel Paket Guide]. 22 | 23 | ## Installation 24 | 25 | Pull in the package through Composer. 26 | 27 | ```shell script 28 | $ composer require cybercog/laravel-paket --dev 29 | ``` 30 | 31 | Run Artisan `paket:setup` command to publish Paket assets to `public/vendor/paket` directory & create `storage/paket` directory for terminal job logs. 32 | 33 | ```shell script 34 | $ php artisan paket:setup 35 | ``` 36 | 37 | ## Upgrading 38 | 39 | When upgrading Paket, you should re-publish assets to `public/vendor/paket` directory with force setup command. 40 | 41 | ```shell script 42 | $ php artisan paket:setup --force 43 | ``` 44 | 45 | ## Quick Start 46 | 47 | Run local development server. 48 | 49 | ```shell script 50 | $ php artisan serve 51 | ``` 52 | 53 | Go to URL [http://localhost:8000/paket](http://localhost:8000/paket) in your browser. 54 | 55 | You will only be able to access this dashboard when `APP_ENV=local` is set in `.env` file. 56 | 57 | ### Dashboard 58 | 59 | ![Laravel Paket Dashboard](https://user-images.githubusercontent.com/1849174/64434687-ca25f580-d0c9-11e9-95c4-8df1f2bd02ff.png) 60 | 61 | ### Laravel Ecosystem 62 | 63 | ![Laravel Ecosystem](https://user-images.githubusercontent.com/1849174/64430016-005e7780-d0c0-11e9-8929-7667dcfc985e.png) 64 | 65 | ### Composer Requirements 66 | 67 | ![Laravel Paket Requirements](https://user-images.githubusercontent.com/1849174/64429876-aa89cf80-d0bf-11e9-939e-20107f6bab62.png) 68 | 69 | ### Terminal Jobs 70 | 71 | ![Laravel Paket Jobs](https://user-images.githubusercontent.com/1849174/64499584-4c790a00-d2c2-11e9-902d-cfed49be1d98.png) 72 | 73 | ### Terminal Job Details 74 | 75 | ![Laravel Paket Job](https://user-images.githubusercontent.com/1849174/64499560-38cda380-d2c2-11e9-9ea9-11491865ec6f.png) 76 | 77 | ## License 78 | 79 | - `Laravel Paket` package is open-sourced software licensed under the [MIT License](LICENSE) by [Anton Komarev]. 80 | - `Laravel Paket` logo by [Caneco]. 81 | 82 | ## About CyberCog 83 | 84 | [CyberCog] is a Social Unity of enthusiasts. Research best solutions in product & software development is our passion. 85 | 86 | - [Follow us on Twitter] 87 | 88 | CyberCog 89 | 90 | [Anton Komarev]: https://komarev.com 91 | [Caneco]: http://twitter.com/caneco 92 | [CyberCog]: https://cybercog.su 93 | [Follow us on Twitter]: https://twitter.com/cybercog 94 | [Laravel Paket Guide]: https://laravel-paket.readme.io/docs 95 | [Tailwind CSS]: https://github.com/tailwindcss/tailwindcss 96 | -------------------------------------------------------------------------------- /resources/js/screens/requirements/index.vue: -------------------------------------------------------------------------------- 1 | 66 | 67 | 96 | -------------------------------------------------------------------------------- /resources/js/screens/jobs/item.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 119 | -------------------------------------------------------------------------------- /src/PaketServiceProvider.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Cog\Laravel\Paket; 15 | 16 | use Cog\Contracts\Paket\Job\Repositories\Job as JobRepositoryContract; 17 | use Cog\Laravel\Paket\Console\Commands\Setup; 18 | use Cog\Laravel\Paket\Job\Events\JobHasBeenCreated; 19 | use Cog\Laravel\Paket\Job\Listeners\JobRunner; 20 | use Cog\Laravel\Paket\Job\Repositories\JobFileRepository; 21 | use Cog\Laravel\Paket\Support\Composer; 22 | use Illuminate\Filesystem\Filesystem; 23 | use Illuminate\Support\Facades\Config; 24 | use Illuminate\Support\Facades\Event; 25 | use Illuminate\Support\Facades\Route; 26 | use Illuminate\Support\ServiceProvider; 27 | 28 | final class PaketServiceProvider extends ServiceProvider 29 | { 30 | public function register(): void 31 | { 32 | $this->configure(); 33 | $this->registerConsoleCommands(); 34 | } 35 | 36 | public function boot(): void 37 | { 38 | $this->registerMiddlewareGroups(); 39 | $this->registerPublishes(); 40 | $this->registerResources(); 41 | $this->registerRoutes(); 42 | $this->registerBindings(); 43 | $this->registerListeners(); 44 | } 45 | 46 | private function getRouteConfiguration(): array 47 | { 48 | return [ 49 | 'namespace' => 'Cog\Laravel\Paket\Http\Controllers', 50 | 'prefix' => ltrim(Config::get('paket.base_uri', 'paket'), '/'), 51 | 'middleware' => 'paket', 52 | ]; 53 | } 54 | 55 | /** 56 | * Merge Paket configuration with the application configuration. 57 | * 58 | * @return void 59 | */ 60 | private function configure(): void 61 | { 62 | if (!$this->app->configurationIsCached()) { 63 | $this->mergeConfigFrom(__DIR__ . '/../config/paket.php', 'paket'); 64 | } 65 | } 66 | 67 | private function registerMiddlewareGroups(): void 68 | { 69 | Route::middlewareGroup('paket', Config::get('paket.middlewares', [])); 70 | } 71 | 72 | private function registerResources(): void 73 | { 74 | $this->loadViewsFrom(__DIR__ . '/../resources/views', 'paket'); 75 | } 76 | 77 | private function registerPublishes(): void 78 | { 79 | if ($this->app->runningInConsole()) { 80 | $this->publishes([ 81 | __DIR__ . '/../public' => public_path('vendor/paket'), 82 | ], 'paket-assets'); 83 | 84 | $this->publishes([ 85 | __DIR__ . '/../config/paket.php' => config_path('paket.php'), 86 | ], 'paket-config'); 87 | } 88 | } 89 | 90 | private function registerRoutes(): void 91 | { 92 | Route::group($this->getRouteConfiguration(), function () { 93 | $this->loadRoutesFrom(__DIR__ . '/../routes/web.php'); 94 | }); 95 | } 96 | 97 | private function registerConsoleCommands(): void 98 | { 99 | $this->commands([ 100 | Setup::class, 101 | ]); 102 | } 103 | 104 | private function registerBindings(): void 105 | { 106 | $this->app->singleton(Composer::class, function () { 107 | return new Composer( 108 | $this->app->make(Filesystem::class), 109 | base_path(), 110 | storage_path('paket/jobs'), 111 | Config::get('paket.composer_path', '/usr/bin/composer') 112 | ); 113 | }); 114 | 115 | $this->app->singleton(JobRepositoryContract::class, function () { 116 | return new JobFileRepository( 117 | $this->app->make(Filesystem::class), 118 | storage_path('paket') 119 | ); 120 | }); 121 | } 122 | 123 | private function registerListeners(): void 124 | { 125 | Event::listen(JobHasBeenCreated::class, JobRunner::class); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /resources/js/screens/jobs/index.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 119 | -------------------------------------------------------------------------------- /resources/js/components/PaketCard.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 132 | -------------------------------------------------------------------------------- /resources/js/components/TopMenu.vue: -------------------------------------------------------------------------------- 1 | 46 | 47 | 90 | -------------------------------------------------------------------------------- /src/Http/Controllers/Api/Jobs/PostAction.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Cog\Laravel\Paket\Http\Controllers\Api\Jobs; 15 | 16 | use Cog\Contracts\Paket\Job\Repositories\Job as JobRepositoryContract; 17 | use Cog\Contracts\Paket\Requirement\Entities\Requirement as RequirementContract; 18 | use Cog\Laravel\Paket\Job\Entities\Job; 19 | use Cog\Laravel\Paket\Job\Events\JobHasBeenCreated; 20 | use Cog\Laravel\Paket\Process\Entities\Process; 21 | use Cog\Laravel\Paket\Requirement\Entities\Requirement; 22 | use Illuminate\Contracts\Support\Responsable as ResponsableContract; 23 | use Illuminate\Support\Arr; 24 | use Illuminate\Support\Carbon; 25 | use Illuminate\Validation\ValidationException; 26 | use MCStreetguy\ComposerParser\Factory as ComposerParser; 27 | use Ramsey\Uuid\Uuid; 28 | use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; 29 | 30 | final class PostAction 31 | { 32 | private $jobs; 33 | 34 | public function __construct(JobRepositoryContract $jobs) 35 | { 36 | $this->jobs = $jobs; 37 | } 38 | 39 | public function __invoke(PostRequest $request): ResponsableContract 40 | { 41 | $type = $request->input('type'); 42 | $requirement = Requirement::fromArray($request->input('requirement')); 43 | 44 | switch ($type) { 45 | case 'RequirementInstall': 46 | if ($this->isRequirementInstalled($requirement)) { 47 | throw ValidationException::withMessages([ 48 | 'name' => [ 49 | "Package `{$requirement}` already installed", 50 | ], 51 | ]); 52 | } 53 | break; 54 | case 'RequirementUninstall': 55 | $requirement = $this->getInstalledRequirement($requirement); 56 | break; 57 | } 58 | 59 | $job = new Job( 60 | $type, 61 | strval(Uuid::uuid4()), 62 | 'Pending', 63 | Carbon::now(), 64 | new Process(), 65 | $requirement 66 | ); 67 | 68 | $this->jobs->store($job); 69 | 70 | event(new JobHasBeenCreated($job)); 71 | 72 | return new PostResponse($job); 73 | } 74 | 75 | private function getInstalledRequirements(): array 76 | { 77 | $lockFile = ComposerParser::parseLockfile(base_path('composer.lock')); 78 | 79 | $packages = $lockFile->getPackages()->getData(); 80 | $devPackages = $lockFile->getPackagesDev()->getData(); 81 | $platform = $lockFile->getPlatform()->getData(); 82 | $devPlatform = $lockFile->getPlatformDev()->getData(); 83 | foreach ($packages as &$package) { 84 | $package['isDevelopment'] = false; 85 | } 86 | foreach ($devPackages as &$package) { 87 | $package['isDevelopment'] = true; 88 | } 89 | $platforms = []; 90 | foreach ($platform as $name => $version) { 91 | $platforms[] = [ 92 | 'name' => $name, 93 | 'version' => $version, 94 | 'isDevelopment' => false, 95 | ]; 96 | } 97 | foreach ($devPlatform as $name => $version) { 98 | $platforms[] = [ 99 | 'name' => $name, 100 | 'version' => $version, 101 | 'isDevelopment' => true, 102 | ]; 103 | } 104 | 105 | return array_merge($packages, $devPackages, $platforms); 106 | } 107 | 108 | private function getInstalledRequirement(RequirementContract $requirement): RequirementContract 109 | { 110 | $installedRequirements = $this->getInstalledRequirements(); 111 | 112 | $installedRequirement = Arr::first($installedRequirements, function (array $value) use ($requirement) { 113 | return $value['name'] === $requirement->getName(); 114 | }); 115 | 116 | if (is_null($installedRequirement)) { 117 | throw new NotFoundHttpException(); 118 | } 119 | 120 | $requirement = Requirement::fromArray($installedRequirement); 121 | 122 | return $requirement; 123 | } 124 | 125 | private function isRequirementInstalled(RequirementContract $requirement): bool 126 | { 127 | $installedRequirements = $this->getInstalledRequirements(); 128 | 129 | $installedRequirement = Arr::first($installedRequirements, function (array $value) use ($requirement) { 130 | return $value['name'] === $requirement->getName(); 131 | }); 132 | 133 | return !is_null($installedRequirement) 134 | && $installedRequirement['version'] === $requirement->getVersion() 135 | && $installedRequirement['isDevelopment'] === $requirement->isDevelopment(); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /resources/js/components/Requirement/Suggestions.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 144 | -------------------------------------------------------------------------------- /src/Job/Repositories/JobFileRepository.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Cog\Laravel\Paket\Job\Repositories; 15 | 16 | use Cog\Contracts\Paket\Job\Entities\Job as JobContract; 17 | use Cog\Contracts\Paket\Job\Repositories\Job as JobRepositoryContract; 18 | use Cog\Laravel\Paket\Job\Entities\Job; 19 | use Illuminate\Contracts\Filesystem\FileNotFoundException; 20 | use Illuminate\Filesystem\Filesystem; 21 | use RuntimeException; 22 | use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; 23 | 24 | final class JobFileRepository implements JobRepositoryContract 25 | { 26 | /** 27 | * The filesystem instance. 28 | * 29 | * @var \Illuminate\Filesystem\Filesystem 30 | */ 31 | private $files; 32 | 33 | /** 34 | * The path to store jobs data. 35 | * 36 | * @var string 37 | */ 38 | private $storagePath; 39 | 40 | /** 41 | * Create a new job file repository instance. 42 | * 43 | * @param \Illuminate\Filesystem\Filesystem $files 44 | * @param string $storagePath 45 | */ 46 | public function __construct(Filesystem $files, string $storagePath) 47 | { 48 | $this->files = $files; 49 | $this->storagePath = $storagePath; 50 | } 51 | 52 | public function all(): iterable 53 | { 54 | $index = $this->getIndex(); 55 | 56 | $jobs = []; 57 | foreach ($index as $job) { 58 | $jobs[] = Job::fromArray($job); 59 | } 60 | 61 | return $jobs; 62 | } 63 | 64 | public function getById(string $id): JobContract 65 | { 66 | $index = $this->getIndex(); 67 | 68 | foreach ($index as $job) { 69 | if ($job['id'] === $id) { 70 | $job['process']['output'] = $this->getJobProcessOutput($job['id']); 71 | 72 | return Job::fromArray($job); 73 | } 74 | } 75 | 76 | throw new NotFoundHttpException("Job with id `{$id}` not found."); 77 | } 78 | 79 | // TODO: [v2.0] Add to contract 80 | public function deleteById(string $id): void 81 | { 82 | $index = $this->getIndex(); 83 | 84 | foreach ($index as $key => $job) { 85 | if ($job['id'] === $id) { 86 | $jobKey = $key; 87 | break; 88 | } 89 | } 90 | 91 | if (!isset($jobKey)) { 92 | throw new NotFoundHttpException("Job with id `{$id}` not found."); 93 | } 94 | 95 | unset($index[$jobKey]); 96 | $index = array_values($index); 97 | 98 | $this->deleteJobProcessOutput($id); 99 | 100 | $this->putIndex($index); 101 | } 102 | 103 | public function store(JobContract $job): void 104 | { 105 | $index = $this->getIndex(); 106 | 107 | $index[] = $job->toArray(); 108 | 109 | $this->putIndex($index); 110 | } 111 | 112 | public function changeJobStatus(JobContract $job, string $statusName): void 113 | { 114 | $names = [ 115 | 'Pending', 116 | 'Running', 117 | 'Success', 118 | 'Failed', 119 | ]; 120 | 121 | if (!in_array($statusName, $names)) { 122 | // TODO: Throw custom exception 123 | throw new RuntimeException('Unknown job status'); 124 | } 125 | 126 | $index = $this->getIndex(); 127 | 128 | foreach ($index as $key => $record) { 129 | if ($record['id'] === $job->getId()) { 130 | $index[$key]['status'] = $statusName; 131 | break; 132 | } 133 | } 134 | 135 | $this->putIndex($index); 136 | } 137 | 138 | public function changeJobExitCode(JobContract $job, int $exitCode): void 139 | { 140 | $index = $this->getIndex(); 141 | 142 | foreach ($index as $key => $record) { 143 | if ($record['id'] === $job->getId()) { 144 | $index[$key]['process']['exitCode'] = $exitCode; 145 | break; 146 | } 147 | } 148 | 149 | $this->putIndex($index); 150 | } 151 | 152 | private function getIndex(): array 153 | { 154 | try { 155 | return json_decode($this->files->get($this->getIndexFilePath(), $isLocked = true), true); 156 | } catch (FileNotFoundException $exception) { 157 | return []; 158 | } 159 | } 160 | 161 | private function putIndex(iterable $index): void 162 | { 163 | $this->files->put($this->getIndexFilePath(), json_encode($index)); 164 | } 165 | 166 | private function getJobProcessOutput(string $jobId): string 167 | { 168 | $path = sprintf('%s/jobs/%s.log', $this->storagePath, $jobId); 169 | 170 | try { 171 | return $this->files->get($path); 172 | } catch (FileNotFoundException $exception) { 173 | return ''; 174 | } 175 | } 176 | 177 | private function deleteJobProcessOutput(string $jobId): void 178 | { 179 | $path = sprintf('%s/jobs/%s.log', $this->storagePath, $jobId); 180 | $this->files->delete($path); 181 | } 182 | 183 | private function getIndexFilePath(): string 184 | { 185 | return $this->storagePath . '/jobs.json'; 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /resources/js/screens/dashboard/index.vue: -------------------------------------------------------------------------------- 1 | 81 | 82 | 147 | -------------------------------------------------------------------------------- /resources/js/store.js: -------------------------------------------------------------------------------- 1 | import Axios from 'axios'; 2 | import Vue from 'vue'; 3 | import Vuex from 'vuex'; 4 | 5 | Vue.use(Vuex); 6 | 7 | const state = { 8 | isAutoRefreshingJobs: false, 9 | isInstallerLocked: false, 10 | installerCurrentJob: null, 11 | requirements: [], 12 | requirementSuggestions: [], 13 | repositories: [], 14 | jobs: [], 15 | protectedRequirements: [ 16 | 'php', 17 | 'cybercog/laravel-paket', 18 | 'laravel/framework', 19 | ], 20 | }; 21 | 22 | const mutations = { 23 | lockInstaller(state, job) { 24 | state.isInstallerLocked = true; 25 | state.installerCurrentJob = job; 26 | }, 27 | 28 | unlockInstaller(state) { 29 | state.isInstallerLocked = false; 30 | state.installerCurrentJob = null; 31 | }, 32 | }; 33 | 34 | const actions = { 35 | async collectRequirements() { 36 | const response = await Axios.get(this.getters.getUrl('/api/requirements')); 37 | 38 | this.state.requirements = response.data; 39 | }, 40 | 41 | async postJobs(context, payload) { 42 | // Need this pre-lock to work with `sync` queue 43 | context.commit('lockInstaller', payload); 44 | 45 | try { 46 | const response = await Axios.post(this.getters.getUrl('/api/jobs'), payload); 47 | context.commit('lockInstaller', response.data); 48 | } catch (exception) { 49 | context.commit('unlockInstaller'); 50 | } 51 | 52 | this.dispatch('collectRequirements'); 53 | this.dispatch('collectJobs'); 54 | }, 55 | 56 | async deleteJobs(context, payload) { 57 | await Axios.delete(this.getters.getUrl(`/api/jobs/${payload.id}`)); 58 | }, 59 | 60 | async collectRepositories() { 61 | const url = this.getters.getUrl('/api/repositories'); 62 | 63 | try { 64 | const response = await Axios.get(url); 65 | 66 | if (response.status === 200) { 67 | this.state.repositories = response.data; 68 | } 69 | } catch (exception) { 70 | console.warn(`Cannot fetch ${url}`); 71 | } 72 | }, 73 | 74 | async collectJobs() { 75 | const url = this.getters.getUrl('/api/jobs'); 76 | 77 | try { 78 | const response = await Axios.get(url); 79 | 80 | if (response.status === 200) { 81 | this.state.jobs = response.data.reverse(); 82 | } 83 | } catch (exception) { 84 | console.warn(`Cannot fetch ${url}`); 85 | } 86 | }, 87 | 88 | async autoRefreshJobs(context) { 89 | if (context.isAutoRefreshingJobs) { 90 | return; 91 | } 92 | 93 | context.isAutoRefreshingJobs = true; 94 | 95 | setTimeout(async () => { 96 | await context.dispatch('collectJobs'); 97 | 98 | if (context.getters.getActiveJobs().length > 0) { 99 | await context.dispatch('autoRefreshJobs'); 100 | } else { 101 | await context.dispatch('collectRequirements'); 102 | context.commit('unlockInstaller'); 103 | context.isAutoRefreshingJobs = false; 104 | } 105 | }, 1000); 106 | }, 107 | 108 | async collectRequirementSuggestions(context, payload) { 109 | const response = await Axios.get(`https://packagist.org/search.json?q=${payload.query}`); 110 | 111 | context.state.requirementSuggestions = response.data.results; 112 | }, 113 | 114 | clearRequirementSuggestions(context) { 115 | context.state.requirementSuggestions = []; 116 | }, 117 | }; 118 | 119 | const getters = { 120 | isRequirementInstalled: (state) => (name) => { 121 | return state.requirements['roots']['essential'].filter(requirement => requirement.name === name).length > 0 122 | || state.requirements['roots']['dev'].filter(requirement => requirement.name === name).length > 0 123 | || state.requirements['dependencies']['essential'].filter(requirement => requirement.name === name).length > 0 124 | || state.requirements['dependencies']['dev'].filter(requirement => requirement.name === name).length > 0; 125 | }, 126 | 127 | isNotProtectedRequirement: (state) => (name) => { 128 | return state.protectedRequirements.indexOf(name) === -1; 129 | }, 130 | 131 | getUrl: () => (uri) => { 132 | return window.location.origin + '/' + window.Paket.baseUri + uri; 133 | }, 134 | 135 | getRepositories: (state, getters) => () => { 136 | return state.repositories; 137 | }, 138 | 139 | getJobs: (state, getters) => () => { 140 | return state.jobs; 141 | }, 142 | 143 | getJob: (state, getters) => async (jobId) => { 144 | const url = getters.getUrl(`/api/jobs/${jobId}`); 145 | 146 | try { 147 | return await Axios.get(url); 148 | } catch (exception) { 149 | console.warn(`Cannot fetch ${url}`); 150 | } 151 | }, 152 | 153 | getRequirementJobs: (state, getters) => (requirement) => { 154 | return getters 155 | .getJobs() 156 | .filter(job => requirement && job.requirement && job.requirement.name === requirement.name); 157 | }, 158 | 159 | getRequirementActiveJobs: (state, getters) => (requirement) => { 160 | return getters 161 | .getRequirementJobs(requirement) 162 | .filter(job => job.status === 'Pending' || job.status === 'Running'); 163 | }, 164 | 165 | getActiveJobs: (state, getters) => (requirement) => { 166 | return getters 167 | .getJobs() 168 | .filter(job => job.status === 'Pending' || job.status === 'Running'); 169 | }, 170 | 171 | isProcessingRequirement: (state, getters) => (requirement) => { 172 | if (state.installerCurrentJob === null) { 173 | return false; 174 | } 175 | 176 | return state.installerCurrentJob.requirement.name === requirement.name; 177 | }, 178 | }; 179 | 180 | export default new Vuex.Store({ 181 | state, 182 | getters, 183 | actions, 184 | mutations, 185 | }); 186 | -------------------------------------------------------------------------------- /resources/js/screens/laravel/index.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 155 | -------------------------------------------------------------------------------- /src/Support/Composer.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Cog\Laravel\Paket\Support; 15 | 16 | use Cog\Contracts\Paket\Job\Entities\Job as JobContract; 17 | use Cog\Contracts\Paket\Job\Exceptions\JobFailed; 18 | use Cog\Contracts\Paket\Requirement\Entities\Requirement as RequirementContract; 19 | use Illuminate\Filesystem\Filesystem; 20 | use Illuminate\Support\ProcessUtils; 21 | use Symfony\Component\Process\PhpExecutableFinder; 22 | use Symfony\Component\Process\Process; 23 | 24 | final class Composer 25 | { 26 | /** 27 | * The filesystem instance. 28 | * 29 | * @var \Illuminate\Filesystem\Filesystem 30 | */ 31 | private $files; 32 | 33 | /** 34 | * The working path to execute Composer from. 35 | * 36 | * @var string 37 | */ 38 | private $workingPath; 39 | 40 | /** 41 | * The logging path to store Composer logs. 42 | * 43 | * @var string 44 | */ 45 | private $loggingPath; 46 | 47 | /** 48 | * The Composer executable file path. 49 | * 50 | * @var string 51 | */ 52 | private $composerPath; 53 | 54 | /** 55 | * Create a new Composer manager instance. 56 | * 57 | * @param \Illuminate\Filesystem\Filesystem $files 58 | * @param string $workingPath 59 | * @param string $loggingPath 60 | * @param string $composerPath 61 | */ 62 | public function __construct(Filesystem $files, string $workingPath, string $loggingPath, string $composerPath) 63 | { 64 | $this->files = $files; 65 | $this->workingPath = $workingPath; 66 | $this->loggingPath = $loggingPath; 67 | $this->composerPath = $composerPath; 68 | } 69 | 70 | /** 71 | * Regenerate the Composer autoloader files. 72 | * 73 | * @param string[] $extra 74 | * @return void 75 | */ 76 | public function dumpAutoload(array $extra = []): void 77 | { 78 | $command = array_merge($this->getComposerExecutable(), ['dump-autoload'], $extra); 79 | 80 | $this->getProcess($command)->run(); 81 | } 82 | 83 | /** 84 | * Regenerate the optimized Composer autoloader files. 85 | * 86 | * @return void 87 | */ 88 | public function dumpOptimized(): void 89 | { 90 | $this->dumpAutoload(['--optimize']); 91 | } 92 | 93 | /** 94 | * Install Composer requirement. 95 | * 96 | * @param \Cog\Contracts\Paket\Requirement\Entities\Requirement $requirement 97 | * @param \Cog\Contracts\Paket\Job\Entities\Job $job 98 | * @return void 99 | * 100 | * @throws \Cog\Contracts\Paket\Job\Exceptions\JobFailed 101 | */ 102 | public function install(RequirementContract $requirement, JobContract $job): void 103 | { 104 | $flags = [ 105 | '--no-interaction', 106 | ]; 107 | 108 | if ($requirement->isDevelopment()) { 109 | $flags[] = '--dev'; 110 | } 111 | 112 | $command = sprintf( 113 | '%s require %s %s', 114 | $this->composerPath, 115 | $requirement, 116 | implode(' ', $flags) 117 | ); 118 | 119 | $this->executeCommand($job, $command); 120 | } 121 | 122 | /** 123 | * Uninstall Composer requirement. 124 | * 125 | * @param \Cog\Contracts\Paket\Requirement\Entities\Requirement $requirement 126 | * @param \Cog\Contracts\Paket\Job\Entities\Job $job 127 | * @return void 128 | * 129 | * @throws \Cog\Contracts\Paket\Job\Exceptions\JobFailed 130 | */ 131 | public function uninstall(RequirementContract $requirement, JobContract $job): void 132 | { 133 | $flags = [ 134 | '--no-interaction', 135 | ]; 136 | 137 | if ($requirement->isDevelopment()) { 138 | $flags[] = '--dev'; 139 | } 140 | 141 | $command = sprintf( 142 | '%s remove %s %s', 143 | $this->composerPath, 144 | $requirement->getName(), 145 | implode(' ', $flags) 146 | ); 147 | 148 | $this->executeCommand($job, $command); 149 | } 150 | 151 | /** 152 | * Builds full command, executes it and logs process output. 153 | * 154 | * @param \Cog\Contracts\Paket\Job\Entities\Job $job 155 | * @param string $command 156 | * 157 | * @throws \Cog\Contracts\Paket\Job\Exceptions\JobFailed 158 | */ 159 | private function executeCommand(JobContract $job, string $command): void 160 | { 161 | $jobLogFile = sprintf('%s/%s.log', $this->loggingPath, $job->getId()); 162 | $this->writeLineToLogFile($jobLogFile, "$ {$command}\n"); 163 | 164 | $commands = [ 165 | sprintf('export COMPOSER_HOME=%s', '~/.composer'), 166 | sprintf('cd %s', $this->workingPath), 167 | $command, 168 | ]; 169 | 170 | $fullCommand = implode(' && ', $commands); 171 | $isPtyMode = true; 172 | $timeout = 5 * 60; 173 | 174 | $process = Process::fromShellCommandline($fullCommand); 175 | $process->setPty($isPtyMode); 176 | $process->setTimeout($timeout); 177 | $process->start(); 178 | 179 | foreach ($process as $type => $data) { 180 | $this->writeLineToLogFile($jobLogFile, trim($data)); 181 | } 182 | 183 | $this->writeLineToLogFile($jobLogFile, "\n\nDone. Job exited with {$process->getExitCode()}."); 184 | 185 | if ($process->getExitCode() !== 0) { 186 | throw JobFailed::withExitCode($job, $process->getExitCode()); 187 | } 188 | } 189 | 190 | private function getComposerExecutable(): array 191 | { 192 | if ($this->files->exists($this->workingPath . '/composer.phar')) { 193 | return [$this->getPhpBinary(), 'composer.phar']; 194 | } 195 | 196 | return ['composer']; 197 | } 198 | 199 | private function getPhpBinary(): string 200 | { 201 | return ProcessUtils::escapeArgument((new PhpExecutableFinder)->find(false)); 202 | } 203 | 204 | private function getProcess(array $command): Process 205 | { 206 | return (new Process($command, $this->workingPath))->setTimeout(null); 207 | } 208 | 209 | private function writeLineToLogFile(string $jobLogFile, string $line): void 210 | { 211 | $this->files->append($jobLogFile, $line . "\n"); 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /resources/js/components/Requirement/InstallForm.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 213 | --------------------------------------------------------------------------------