lthn.io/app/Core/Console/Commands/NewProjectCommand.php

371 lines
12 KiB
PHP
Raw Permalink Normal View History

<?php
/*
* Core PHP Framework
*
* Licensed under the European Union Public Licence (EUPL) v1.2.
* See LICENSE file for details.
*/
declare(strict_types=1);
namespace Core\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Process;
use Illuminate\Support\Str;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
/**
* Create a new Core PHP Framework project.
*
* Similar to `laravel new` but creates a project pre-configured
* with Core PHP Framework packages (core, admin, api, mcp).
*
* Usage: php artisan core:new my-project
* php artisan core:new my-project --template=github.com/host-uk/core-template
*/
class NewProjectCommand extends Command
{
/**
* The name and signature of the console command.
*/
protected $signature = 'core:new
{name : The name of the project directory}
{--template= : GitHub template repository (default: host-uk/core-template)}
{--branch=main : Branch to clone from template}
{--no-install : Skip composer install and setup}
{--dev : Install packages in dev mode (with path repos)}
{--force : Overwrite existing directory}';
/**
* The console command description.
*/
protected $description = 'Create a new Core PHP Framework project';
/**
* Default template repository.
*/
protected string $defaultTemplate = 'host-uk/core-template';
/**
* Created files and directories for summary.
*/
protected array $createdPaths = [];
/**
* Execute the console command.
*/
public function handle(): int
{
$name = $this->argument('name');
$directory = getcwd().'/'.$name;
// Validate project name
if (! $this->validateProjectName($name)) {
return self::FAILURE;
}
// Check if directory exists
if (File::isDirectory($directory) && ! $this->option('force')) {
$this->newLine();
$this->components->error("Directory [{$name}] already exists!");
$this->newLine();
$this->components->warn('Use --force to overwrite the existing directory.');
$this->newLine();
return self::FAILURE;
}
$this->newLine();
$this->components->info(' ╔═══════════════════════════════════════════╗');
$this->components->info(' ║ Core PHP Framework Project Creator ║');
$this->components->info(' ╚═══════════════════════════════════════════╝');
$this->newLine();
$template = $this->option('template') ?: $this->defaultTemplate;
$this->components->twoColumnDetail('<fg=cyan>Project Name</>', $name);
$this->components->twoColumnDetail('<fg=cyan>Template</>', $template);
$this->components->twoColumnDetail('<fg=cyan>Directory</>', $directory);
$this->newLine();
try {
// Step 1: Create project from template
$this->components->task('Creating project from template', function () use ($directory, $template, $name) {
return $this->createFromTemplate($directory, $template, $name);
});
// Step 2: Install dependencies
if (! $this->option('no-install')) {
$this->components->task('Installing Composer dependencies', function () use ($directory) {
return $this->installDependencies($directory);
});
// Step 3: Run core:install
$this->components->task('Running framework installation', function () use ($directory) {
return $this->runCoreInstall($directory);
});
}
// Success!
$this->newLine();
$this->components->info(' ✓ Project created successfully!');
$this->newLine();
$this->components->info(' Next steps:');
$this->line(" <fg=gray>1.</> cd {$name}");
if ($this->option('no-install')) {
$this->line(' <fg=gray>2.</> composer install');
$this->line(' <fg=gray>3.</> php artisan core:install');
$this->line(' <fg=gray>4.</> php artisan serve');
} else {
$this->line(' <fg=gray>2.</> php artisan serve');
}
$this->newLine();
$this->showPackageInfo();
return self::SUCCESS;
} catch (\Throwable $e) {
$this->newLine();
$this->components->error(' Project creation failed: '.$e->getMessage());
$this->newLine();
// Cleanup on failure
if (File::isDirectory($directory)) {
$cleanup = $this->confirm('Remove failed project directory?', true);
if ($cleanup) {
File::deleteDirectory($directory);
$this->components->info(' Cleaned up project directory.');
}
}
return self::FAILURE;
}
}
/**
* Validate project name.
*/
protected function validateProjectName(string $name): bool
{
if (empty($name)) {
$this->components->error('Project name cannot be empty');
return false;
}
if (! preg_match('/^[a-z0-9_-]+$/i', $name)) {
$this->components->error('Project name can only contain letters, numbers, hyphens, and underscores');
return false;
}
if (in_array(strtolower($name), ['vendor', 'app', 'test', 'tests', 'src', 'public'])) {
$this->components->error("Project name '{$name}' is reserved");
return false;
}
return true;
}
/**
* Create project from template repository.
*/
protected function createFromTemplate(string $directory, string $template, string $projectName): bool
{
$branch = $this->option('branch');
// If force, delete existing directory
if ($this->option('force') && File::isDirectory($directory)) {
File::deleteDirectory($directory);
}
// Check if template is a URL or repo slug
$templateUrl = $this->resolveTemplateUrl($template);
// Clone the template
$result = Process::run("git clone --branch {$branch} --single-branch --depth 1 {$templateUrl} {$directory}");
if (! $result->successful()) {
throw new \RuntimeException("Failed to clone template: {$result->errorOutput()}");
}
// Remove .git directory to make it a fresh repo
File::deleteDirectory("{$directory}/.git");
// Update composer.json with project name
$this->updateComposerJson($directory, $projectName);
// Initialize new git repository
Process::run("cd {$directory} && git init");
Process::run("cd {$directory} && git add .");
Process::run("cd {$directory} && git commit -m \"Initial commit from Core PHP Framework template\"");
return true;
}
/**
* Resolve template to full git URL.
*/
protected function resolveTemplateUrl(string $template): string
{
// If already a URL, return as-is
if (str_starts_with($template, 'http://') || str_starts_with($template, 'https://')) {
return $template;
}
// If contains .git, treat as SSH URL
if (str_contains($template, '.git')) {
return $template;
}
// Otherwise, assume GitHub slug
return "https://github.com/{$template}.git";
}
/**
* Update composer.json with project name.
*/
protected function updateComposerJson(string $directory, string $projectName): void
{
$composerPath = "{$directory}/composer.json";
if (! File::exists($composerPath)) {
return;
}
$composer = json_decode(File::get($composerPath), true);
$composer['name'] = $this->generateComposerName($projectName);
$composer['description'] = "Core PHP Framework application - {$projectName}";
// Update namespace if using default App namespace
if (isset($composer['autoload']['psr-4']['App\\'])) {
$studlyName = Str::studly($projectName);
$composer['autoload']['psr-4']["{$studlyName}\\"] = 'app/';
unset($composer['autoload']['psr-4']['App\\']);
}
File::put($composerPath, json_encode($composer, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)."\n");
}
/**
* Generate composer package name from project name.
*/
protected function generateComposerName(string $projectName): string
{
$vendor = $this->ask('Composer vendor name', 'my-company');
$package = Str::slug($projectName);
return "{$vendor}/{$package}";
}
/**
* Install composer dependencies.
*/
protected function installDependencies(string $directory): bool
{
$composerBin = $this->findComposer();
$command = $this->option('dev')
? "{$composerBin} install --prefer-source"
: "{$composerBin} install";
$result = Process::run("cd {$directory} && {$command}");
if (! $result->successful()) {
throw new \RuntimeException("Composer install failed: {$result->errorOutput()}");
}
return true;
}
/**
* Run core:install command.
*/
protected function runCoreInstall(string $directory): bool
{
$result = Process::run("cd {$directory} && php artisan core:install --no-interaction");
if (! $result->successful()) {
throw new \RuntimeException("core:install failed: {$result->errorOutput()}");
}
return true;
}
/**
* Find the composer binary.
*/
protected function findComposer(): string
{
// Check if composer is in PATH
$result = Process::run('which composer');
if ($result->successful()) {
return trim($result->output());
}
// Check common locations
$locations = [
'/usr/local/bin/composer',
'/usr/bin/composer',
$_SERVER['HOME'].'/.composer/composer.phar',
];
foreach ($locations as $location) {
if (File::exists($location)) {
return $location;
}
}
return 'composer'; // Fallback, will fail if not in PATH
}
/**
* Show package information.
*/
protected function showPackageInfo(): void
{
$this->components->info(' 📦 Installed Core PHP Packages:');
$this->components->twoColumnDetail(' <fg=cyan>host-uk/core</>', 'Core framework components');
$this->components->twoColumnDetail(' <fg=cyan>host-uk/core-admin</>', 'Admin panel & Livewire modals');
$this->components->twoColumnDetail(' <fg=cyan>host-uk/core-api</>', 'REST API with scopes & webhooks');
$this->components->twoColumnDetail(' <fg=cyan>host-uk/core-mcp</>', 'Model Context Protocol tools');
$this->newLine();
$this->components->info(' 📚 Documentation:');
$this->components->twoColumnDetail(' <fg=yellow>https://github.com/host-uk/core-php</>', 'GitHub Repository');
$this->components->twoColumnDetail(' <fg=yellow>https://docs.core-php.dev</>', 'Official Docs (future)');
$this->newLine();
}
/**
* Get shell completion suggestions.
*/
public function complete(
CompletionInput $input,
CompletionSuggestions $suggestions
): void {
if ($input->mustSuggestArgumentValuesFor('name')) {
// Suggest common project naming patterns
$suggestions->suggestValues([
'my-app',
'api-service',
'admin-panel',
'saas-platform',
]);
}
if ($input->mustSuggestOptionValuesFor('template')) {
// Suggest known templates
$suggestions->suggestValues([
'host-uk/core-template',
'host-uk/core-api-template',
'host-uk/core-admin-template',
]);
}
}
}