From 086c3e2ebb65229b57ede8adc497fe653f5729e4 Mon Sep 17 00:00:00 2001 From: Greg Sarjeant Date: Tue, 5 Aug 2025 21:48:10 +0000 Subject: [PATCH] Make first time setup more robust. (#58) Split out prerequisite validation from creation. Distinguish system prereqs from application prereqs. Support URL autodetection to eliminate requirement to edit file as part of setup. Reviewed-on: https://gitea.subcultureofone.org/greg/tkr/pulls/58 Co-authored-by: Greg Sarjeant Co-committed-by: Greg Sarjeant --- .gitea/workflows/prerequisites.yaml | 16 +- check-prerequisites.php | 21 -- config/init.php | 7 - public/index.php | 32 +- .../AdminController/AdminController.php | 15 + src/Framework/Prerequisites/Prerequisites.php | 289 +++++++++++------- src/Framework/Router/Router.php | 4 +- src/Framework/Util/Util.php | 38 +++ src/Model/ConfigModel/ConfigModel.php | 3 - tkr-setup.php | 214 +++++++++++++ 10 files changed, 473 insertions(+), 166 deletions(-) delete mode 100644 check-prerequisites.php delete mode 100644 config/init.php create mode 100644 tkr-setup.php diff --git a/.gitea/workflows/prerequisites.yaml b/.gitea/workflows/prerequisites.yaml index 371eb9f..e58528a 100644 --- a/.gitea/workflows/prerequisites.yaml +++ b/.gitea/workflows/prerequisites.yaml @@ -20,14 +20,14 @@ jobs: run: | if [[ "${{ matrix.php }}" < "8.2" ]]; then echo "Testing PHP ${{ matrix.php }} - should fail" - if php check-prerequisites.php; then + if php tkr-setup.php --validate-only; then echo "ERROR: Should have failed with PHP ${{ matrix.php }}" exit 1 fi echo "āœ“ Correctly failed with old PHP version" else echo "Testing PHP ${{ matrix.php }} - should pass" - php check-prerequisites.php + php tkr-setup.php --validate-only echo "āœ“ Correctly passed with supported PHP version" fi @@ -66,7 +66,7 @@ jobs: - name: Test failure with missing extensions run: | echo "Testing with base PHP - should fail" - if php check-prerequisites.php; then + if php tkr-setup.php --validate-only; then echo "ERROR: Should have failed with missing extensions" exit 1 fi @@ -86,7 +86,7 @@ jobs: - name: Test still fails without SQLite run: | echo "Testing with PDO but no SQLite - should still fail" - if php check-prerequisites.php; then + if php tkr-setup.php --validate-only; then echo "ERROR: Should have failed without SQLite" exit 1 fi @@ -105,7 +105,7 @@ jobs: - name: Test now passes with required extensions run: | echo "Testing with all required extensions - should pass" - php check-prerequisites.php + php tkr-setup.php --validate-only echo "āœ“ All required extensions detected correctly" - name: Install recommended extensions and retest @@ -117,7 +117,7 @@ jobs: else apt-get install -y php-mbstring php-curl fi - php check-prerequisites.php + php tkr-setup.php --validate-only echo "āœ“ Recommended extensions also detected" test-permission-scenarios: @@ -142,7 +142,7 @@ jobs: chown root:root storage # Run as the non-root user - should fail - if su testuser -c "php check-prerequisites.php"; then + if su testuser -c "php tkr-setup.php --validate-only"; then echo "ERROR: Should have failed with unwritable storage" exit 1 fi @@ -157,7 +157,7 @@ jobs: rm -rf src templates # Should fail - if php check-prerequisites.php; then + if php tkr-setup.php --validate-only; then echo "ERROR: Should have failed with missing directories" exit 1 fi \ No newline at end of file diff --git a/check-prerequisites.php b/check-prerequisites.php deleted file mode 100644 index 81fbd78..0000000 --- a/check-prerequisites.php +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env php -validate(); - -// Exit with appropriate code for shell scripts -if (php_sapi_name() === 'cli') { - exit(count($prerequisites->getErrors()) > 0 ? 1 : 0); -} \ No newline at end of file diff --git a/config/init.php b/config/init.php deleted file mode 100644 index 1b4e9b5..0000000 --- a/config/init.php +++ /dev/null @@ -1,7 +0,0 @@ - 'http://localhost', - 'base_path' => '/tkr/', -]; diff --git a/public/index.php b/public/index.php index 254143b..57f646c 100644 --- a/public/index.php +++ b/public/index.php @@ -22,27 +22,37 @@ include_once(dirname(dirname(__FILE__)) . "/config/bootstrap.php"); * Validate application state before processing request */ -// Check prerequisites (includes database connection and migrations) +// Check system requirements first $prerequisites = new Prerequisites(); -if (!$prerequisites->validate()) { +if (!$prerequisites->validateSystem()) { $prerequisites->generateWebSummary(); exit; } +// Check application state and create missing components if needed +if (!$prerequisites->validateApplication()) { + if (!$prerequisites->createMissing()) { + $prerequisites->generateWebSummary(); + exit; + } +} + // Get the working database connection from prerequisites $db = $prerequisites->getDatabase(); -// Make sure the initial setup is complete unless we're already heading to setup -if (!(preg_match('/setup$/', $path))) { +// Check if setup is complete (user exists and URL is configured) +if (!(preg_match('/tkr-setup$/', $path))) { try { - // Make sure required tables (user, settings) are populated $user_count = (int) $db->query("SELECT COUNT(*) FROM user")->fetchColumn(); - $settings_count = (int) $db->query("SELECT COUNT(*) FROM settings")->fetchColumn(); - - // If either required table has no records, redirect to setup. - if ($user_count === 0 || $settings_count === 0){ - $init = require APP_ROOT . '/config/init.php'; - header('Location: ' . $init['base_path'] . 'setup'); + $config = (new ConfigModel($db))->get(); + + $hasUser = $user_count > 0; + $hasUrl = !empty($config->baseUrl) && !empty($config->basePath); + + if (!$hasUser || !$hasUrl) { + // Redirect to setup with auto-detected URL + $autodetected = Util::getAutodetectedUrl(); + header('Location: ' . $autodetected['fullUrl'] . '/tkr-setup'); exit; } } catch (Exception $e) { diff --git a/src/Controller/AdminController/AdminController.php b/src/Controller/AdminController/AdminController.php index afccd5d..f86878b 100644 --- a/src/Controller/AdminController/AdminController.php +++ b/src/Controller/AdminController/AdminController.php @@ -9,6 +9,21 @@ class AdminController extends Controller { public function showSetup(){ $data = $this->getAdminData(true); + + // Auto-detect URL and pre-fill if not already configured + if (empty($data['config']->baseUrl) || empty($data['config']->basePath)) { + $autodetected = Util::getAutodetectedUrl(); + $data['autodetectedUrl'] = $autodetected; + + // Pre-fill empty values with auto-detected ones + if (empty($data['config']->baseUrl)) { + $data['config']->baseUrl = $autodetected['baseUrl']; + } + if (empty($data['config']->basePath)) { + $data['config']->basePath = $autodetected['basePath']; + } + } + $this->render("admin.php", $data); } diff --git a/src/Framework/Prerequisites/Prerequisites.php b/src/Framework/Prerequisites/Prerequisites.php index 7b064c3..49ef923 100644 --- a/src/Framework/Prerequisites/Prerequisites.php +++ b/src/Framework/Prerequisites/Prerequisites.php @@ -5,7 +5,6 @@ * This class checks all system requirements for tkr and provides * detailed logging of any missing components or configuration issues. * - * ZERO DEPENDENCIES - Uses only core PHP functions available since PHP 5.3 */ class Prerequisites { @@ -17,6 +16,12 @@ class Prerequisites { private $isCli; private $isWeb; private $database = null; + private $storageSubdirs = [ + 'storage/db', + 'storage/logs', + 'storage/upload', + 'storage/upload/css', + ]; public function __construct() { $this->isCli = php_sapi_name() === 'cli'; @@ -175,7 +180,7 @@ class Prerequisites { return $allPresent; } - private function checkStoragePermissions() { + private function checkExistingStoragePermissions() { // Issue a warning if running as root in CLI context // Write out guidance for storage directory permissions // if running the CLI script as root (since it will always appear to be writable) @@ -195,20 +200,68 @@ class Prerequisites { ); } - $storageDirs = array( - 'storage', - 'storage/db', - 'storage/logs', - 'storage/upload', - 'storage/upload/css' + $storageDirs = array_merge( + array('storage'), + $this->storageSubdirs ); $allWritable = true; foreach ($storageDirs as $dir) { $path = $this->baseDir . '/' . $dir; + // Only check directories that exist - missing ones are handled in application validation + if (is_dir($path)) { + $writable = is_writable($path); + $permissions = substr(sprintf('%o', fileperms($path)), -4); + + $this->addCheck( + "Storage Permissions: {$dir}", + $writable, + $writable ? "Writable (permissions: {$permissions})" : "Not writable (permissions: {$permissions})", + $writable ? 'info' : 'error' + ); + + if (!$writable) { + $allWritable = false; + } + } + } + + return $allWritable; + } + + private function checkStorageDirectoriesExist() { + $allPresent = true; + foreach ($this->storageSubdirs as $dir) { + $path = $this->baseDir . '/' . $dir; + $exists = is_dir($path); + + $this->addCheck( + "Storage Directory: {$dir}", + $exists, + $exists ? "Present" : "Missing - will be created during setup", + $exists ? 'info' : 'info' // Not an error - can be auto-created + ); + + if (!$exists) { + $allPresent = false; + } + } + + return $allPresent; + } + + private function createStorageDirectories() { + $storageDirs = array_merge( + array('storage'), + $this->storageSubdirs + ); + + $allCreated = true; + foreach ($storageDirs as $dir) { + $path = $this->baseDir . '/' . $dir; + if (!is_dir($path)) { - // Try to create the directory $created = @mkdir($path, 0770, true); if ($created) { $this->addCheck( @@ -223,27 +276,18 @@ class Prerequisites { "Could not create directory: {$dir}", 'error' ); - $allWritable = false; - continue; + $allCreated = false; } - } - - $writable = is_writable($path); - $permissions = substr(sprintf('%o', fileperms($path)), -4); - - $this->addCheck( - "Storage Permissions: {$dir}", - $writable, - $writable ? "Writable (permissions: {$permissions})" : "Not writable (permissions: {$permissions})", - $writable ? 'info' : 'error' - ); - - if (!$writable) { - $allWritable = false; + } else { + $this->addCheck( + "Storage Directory: {$dir}", + true, + "Already exists" + ); } } - return $allWritable; + return $allCreated; } private function checkWebServerConfig() { @@ -283,73 +327,19 @@ class Prerequisites { return true; } - private function checkConfiguration() { - $configFile = $this->baseDir . '/config/init.php'; - $configExists = file_exists($configFile); - - if (!$configExists) { - $this->addCheck( - 'Configuration File', - false, - 'config/init.php not found', - 'error' - ); - return false; - } - - try { - $config = include $configFile; - $hasBaseUrl = isset($config['base_url']) && !empty($config['base_url']); - $hasBasePath = isset($config['base_path']) && !empty($config['base_path']); - - $this->addCheck( - 'Configuration File', - true, - 'config/init.php exists and is readable' - ); - - $this->addCheck( - 'Base URL Configuration', - $hasBaseUrl, - $hasBaseUrl ? "Set to: {$config['base_url']}" : 'Not configured', - $hasBaseUrl ? 'info' : 'warning' - ); - - $this->addCheck( - 'Base Path Configuration', - $hasBasePath, - $hasBasePath ? "Set to: {$config['base_path']}" : 'Not configured', - $hasBasePath ? 'info' : 'warning' - ); - - return $hasBaseUrl && $hasBasePath; - - } catch (Exception $e) { - $this->addCheck( - 'Configuration File', - false, - 'Error reading config/init.php: ' . $e->getMessage(), - 'error' - ); - return false; - } - } private function checkDatabase() { $dbFile = $this->baseDir . '/storage/db/tkr.sqlite'; $dbDir = dirname($dbFile); if (!is_dir($dbDir)) { - $created = @mkdir($dbDir, 0770, true); - if (!$created) { - $this->addCheck( - 'Database Directory', - false, - 'Could not create storage/db directory', - 'error' - ); - return false; - } + $this->addCheck( + 'Database Directory', + false, + 'Database directory does not exist', + 'error' + ); + return false; } $canCreateDb = is_writable($dbDir); @@ -370,39 +360,75 @@ class Prerequisites { $dbReadable && $dbWritable ? 'Exists and is accessible' : 'Exists but has permission issues', $dbReadable && $dbWritable ? 'info' : 'error' ); + + if ($dbReadable && $dbWritable) { + // Test database connection + try { + $db = new PDO("sqlite:" . $dbFile); + $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + $db->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC); + + // Test basic query to ensure database is functional + $db->query("SELECT 1")->fetchColumn(); + + $this->addCheck( + 'Database Connection', + true, + 'Successfully connected to database' + ); + + // Store working database connection + $this->database = $db; + + return true; + + } catch (PDOException $e) { + $this->addCheck( + 'Database Connection', + false, + 'Failed to connect: ' . $e->getMessage(), + 'error' + ); + return false; + } + } else { + return false; + } } else { $this->addCheck( 'Database File', - true, - 'Will be created on first run' + $canCreateDb, + $canCreateDb ? 'Will be created during setup' : 'Cannot create - directory not writable', + $canCreateDb ? 'info' : 'error' ); + return $canCreateDb; } + } - if (!$canCreateDb) { - return false; - } + private function createDatabase() { + $dbFile = $this->baseDir . '/storage/db/tkr.sqlite'; - // Test database connection + // Test database connection (will create file if needed) try { $db = new PDO("sqlite:" . $dbFile); $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $db->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC); - + // Test basic query to ensure database is functional $db->query("SELECT 1")->fetchColumn(); - + $this->addCheck( 'Database Connection', true, 'Successfully connected to database' ); - + // Store working database connection $this->database = $db; - - // Test migrations - return $this->checkMigrations($db); - + + // Run migrations + return $this->applyMigrations($db); + } catch (PDOException $e) { $this->addCheck( 'Database Connection', @@ -413,19 +439,19 @@ class Prerequisites { return false; } } - - private function checkMigrations($db) { + + private function applyMigrations($db) { try { $migrator = new Migrator($db); $migrator->migrate(); - + $this->addCheck( 'Database Migrations', true, 'All database migrations applied successfully' ); return true; - + } catch (Exception $e) { $this->addCheck( 'Database Migrations', @@ -437,23 +463,20 @@ class Prerequisites { } } - // validate prereqs - // runs on each request and can be run from CLI - public function validate(): bool { - $this->log("=== tkr prerequisites check started at " . date('Y-m-d H:i:s') . " ===", true); + // Validate system requirements that can't be fixed by the script + public function validateSystem(): bool { + $this->log("=== tkr system validation started at " . date('Y-m-d H:i:s') . " ===", true); if ($this->isCli) { - $this->log("\nšŸ” Validating prerequisites...\n"); + $this->log("\nšŸ” Validating system requirements...\n"); } $results = array( 'php_version' => $this->checkPhpVersion(), 'critical_extensions' => $this->checkRequiredExtensions(), 'directory_structure' => $this->checkDirectoryStructure(), - 'storage_permissions' => $this->checkStoragePermissions(), - 'web_server' => $this->checkWebServerConfig(), - 'configuration' => $this->checkConfiguration(), - 'database' => $this->checkDatabase() + 'existing_storage_permissions' => $this->checkExistingStoragePermissions(), + 'web_server' => $this->checkWebServerConfig() ); // Check recommended extensions too @@ -467,6 +490,44 @@ class Prerequisites { return count($this->errors) === 0; } + // Validate application state - things that can be fixed + public function validateApplication(): bool { + $currentErrors = count($this->errors); + + if ($this->isCli) { + $this->log("\nšŸ” Validating application state...\n"); + } + + $results = array( + 'storage_directories' => $this->checkStorageDirectoriesExist(), + 'database' => $this->checkDatabase() + ); + + // Return true if no NEW errors occurred + return count($this->errors) === $currentErrors; + } + + // Create missing application components + public function createMissing(): bool { + $this->log("=== tkr setup started at " . date('Y-m-d H:i:s') . " ===", true); + + if ($this->isCli) { + $this->log("\nšŸš€ Creating missing components...\n"); + } + + $results = array( + 'storage_setup' => $this->createStorageDirectories(), + 'database_setup' => $this->createDatabase() + ); + + if ($this->isCli) { + $this->generateCliSummary($results); + } + + // Return true only if no errors occurred + return count($this->errors) === 0; + } + /** * Display web-friendly error page when minimum requirements aren't met */ @@ -625,7 +686,7 @@ class Prerequisites { public function getWarnings() { return $this->warnings; } - + /** * Get working database connection (only call after validate() returns true) */ diff --git a/src/Framework/Router/Router.php b/src/Framework/Router/Router.php index 28d3dcb..9231e05 100644 --- a/src/Framework/Router/Router.php +++ b/src/Framework/Router/Router.php @@ -20,8 +20,8 @@ class Router { ['logout', 'AuthController@handleLogout', ['GET', 'POST']], ['mood', 'MoodController'], ['mood', 'MoodController@handlePost', ['POST']], - ['setup', 'AdminController@showSetup'], - ['setup', 'AdminController@handleSetup', ['POST']], + ['tkr-setup', 'AdminController@showSetup'], + ['tkr-setup', 'AdminController@handleSetup', ['POST']], ['tick/{id}', 'TickController'], ['css/custom/{filename}.css', 'CssController@serveCustomCss'], ]; diff --git a/src/Framework/Util/Util.php b/src/Framework/Util/Util.php index 2741a42..f82d902 100644 --- a/src/Framework/Util/Util.php +++ b/src/Framework/Util/Util.php @@ -103,4 +103,42 @@ class Util { return $basePath . '/' . $path; } + + /** + * Auto-detect base URL and path from HTTP request headers + * Returns array with baseUrl, basePath, and fullUrl + */ + public static function getAutodetectedUrl(): array { + // Detect base URL + $baseUrl = ($_SERVER['HTTPS'] ?? 'off') === 'on' ? 'https://' : 'http://'; + $baseUrl .= $_SERVER['HTTP_HOST'] ?? 'localhost'; + + // Don't include standard ports in URL + $port = $_SERVER['SERVER_PORT'] ?? null; + if ($port && $port != 80 && $port != 443) { + $baseUrl .= ':' . $port; + } + + // Detect base path from script location + $scriptName = $_SERVER['SCRIPT_NAME'] ?? '/index.php'; + $basePath = dirname($scriptName); + + if ($basePath === '/' || $basePath === '.' || $basePath === '') { + $basePath = '/'; + } else { + $basePath = '/' . trim($basePath, '/') . '/'; + } + + // Construct full URL + $fullUrl = $baseUrl; + if ($basePath !== '/') { + $fullUrl .= ltrim($basePath, '/'); + } + + return [ + 'baseUrl' => $baseUrl, + 'basePath' => $basePath, + 'fullUrl' => rtrim($fullUrl, '/') + ]; + } } \ No newline at end of file diff --git a/src/Model/ConfigModel/ConfigModel.php b/src/Model/ConfigModel/ConfigModel.php index 8d76d9f..d08df4e 100644 --- a/src/Model/ConfigModel/ConfigModel.php +++ b/src/Model/ConfigModel/ConfigModel.php @@ -15,10 +15,7 @@ class ConfigModel { // Instance method that uses injected database public function get(): self { - $init = require APP_ROOT . '/config/init.php'; $c = new self($this->db); - $c->baseUrl = ($c->baseUrl === '') ? $init['base_url'] : $c->baseUrl; - $c->basePath = ($c->basePath === '') ? $init['base_path'] : $c->basePath; $stmt = $this->db->query("SELECT site_title, site_description, diff --git a/tkr-setup.php b/tkr-setup.php new file mode 100644 index 0000000..fde0f1f --- /dev/null +++ b/tkr-setup.php @@ -0,0 +1,214 @@ +#!/usr/bin/env php +validateSystem()) { + echo "\nāŒ System requirements not met. Please resolve the issues above before continuing.\n"; + exit(1); +} + +echo "āœ… System requirements met\n\n"; + +// Check application state +$applicationReady = $prerequisites->validateApplication(); + +if ($applicationReady) { + echo "āœ… All prerequisites satisfied - tkr is ready to run!\n"; +} else { + echo "āš ļø Application components need to be created\n\n"; + if ($validateOnly) { + echo "āš ļø Run 'php tkr-setup.php' (without --validate-only) to complete setup.\n"; + } +} + +// If validate-only flag, exit here +if ($validateOnly) { + // Always exit with success. + // If app configuration needs to be completed, the script can handle that. + exit(0); +} + +// Continue with setup process +$db = null; +try { + if ($applicationReady) { + $db = $prerequisites->getDatabase(); + + // Check if user already exists + $userCount = (int) $db->query("SELECT COUNT(*) FROM user")->fetchColumn(); + if ($userCount > 0) { + echo "āš ļø tkr appears to already be set up.\n"; + echo "Continue anyway? (y/N): "; + $continue = trim(fgets(STDIN)); + if (strtolower($continue) !== 'y') { + echo "Setup cancelled.\n"; + exit(0); + } + echo "\n"; + } + } +} catch (Exception $e) { + // Application not ready - will create below +} + +// If application isn't ready, create missing components +if (!$db) { + echo "Setting up application components...\n"; + if (!$prerequisites->createMissing()) { + echo "āŒ Failed to create application components. Check the errors above.\n"; + exit(1); + } + + try { + $db = $prerequisites->getDatabase(); + echo "āœ… Application components created\n\n"; + } catch (Exception $e) { + echo "āŒ Failed to get database connection: " . $e->getMessage() . "\n"; + exit(1); + } +} + +// Prompt for configuration +echo "šŸ“ Please provide the following information:\n\n"; + +// 1. Site URL (with auto-detect option) +echo "1. Site URL (including base path if not root)\n"; +echo " Examples: https://example.com or https://example.com/tkr\n"; +echo " Leave blank to auto-detect from first web request\n"; +echo " Site URL (optional): "; +$siteUrl = trim(fgets(STDIN)); + +if (empty($siteUrl)) { + echo "āœ… Will auto-detect URL on first web request\n"; + $baseUrl = ''; + $basePath = ''; +} else { + // Parse URL to extract base URL and base path + $parsedUrl = parse_url($siteUrl); + if (!$parsedUrl || !isset($parsedUrl['scheme']) || !isset($parsedUrl['host'])) { + echo "āŒ Invalid URL format\n"; + exit(1); + } + + // Validate host for basic security + if (!preg_match('/^[a-zA-Z0-9.-]+$/', $parsedUrl['host'])) { + echo "āŒ Invalid characters in hostname\n"; + exit(1); + } + + $baseUrl = $parsedUrl['scheme'] . '://' . $parsedUrl['host']; + if (isset($parsedUrl['port']) && $parsedUrl['port'] != 80 && $parsedUrl['port'] != 443) { + $baseUrl .= ':' . $parsedUrl['port']; + } + + $basePath = isset($parsedUrl['path']) ? rtrim($parsedUrl['path'], '/') : ''; + if (empty($basePath)) { + $basePath = '/'; + } else { + $basePath = '/' . trim($basePath, '/') . '/'; + } +} + +echo "\n"; + +// 2. Admin credentials +echo "2. Admin username: "; +$adminUsername = trim(fgets(STDIN)); + +if (empty($adminUsername)) { + echo "āŒ Admin username is required\n"; + exit(1); +} + +echo "3. Admin password: "; +// Hide password input on Unix systems +if (strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') { + system('stty -echo'); +} +$adminPassword = trim(fgets(STDIN)); +if (strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') { + system('stty echo'); +} +echo "\n"; + +if (empty($adminPassword)) { + echo "āŒ Admin password is required\n"; + exit(1); +} + +echo "\n4. Site title (optional, default: 'My tkr Site'): "; +$siteTitle = trim(fgets(STDIN)); +if (empty($siteTitle)) { + $siteTitle = 'My tkr Site'; +} + +echo "\n"; + +// Save configuration +try { + echo "šŸ’¾ Saving configuration...\n"; + + // Create/update settings + $configModel = new ConfigModel($db); + $configModel->siteTitle = $siteTitle; + $configModel->baseUrl = $baseUrl; + $configModel->basePath = $basePath; + $config = $configModel->save(); + + // Create admin user + $userModel = new UserModel($db); + $userModel->username = $adminUsername; + $userModel->display_name = $adminUsername; + $userModel->website = ''; + $userModel->mood = ''; + $user = $userModel->save(); + + // Set admin password + $userModel->setPassword($adminPassword); + + echo "āœ… Configuration saved\n"; + echo "āœ… Admin user created\n\n"; + + echo "šŸŽ‰ Setup complete!\n\n"; + + if (!empty($baseUrl)) { + echo "Your tkr site is ready at: $siteUrl\n"; + } else { + echo "Your tkr site will be ready after you visit it in a web browser\n"; + echo "The URL will be auto-detected on first access\n"; + } + + echo "Login with username: $adminUsername\n\n"; + echo "You can now:\n"; + echo "• Point your web server document root to the 'public/' directory\n"; + echo "• Visit your site and log in\n"; + echo "• Customize additional settings through the admin interface\n\n"; + +} catch (Exception $e) { + echo "āŒ Setup failed: " . $e->getMessage() . "\n"; + exit(1); +} \ No newline at end of file