diff --git a/cli/src/commands/wheels/base.cfc b/cli/src/commands/wheels/base.cfc index 5041dccf7..c1b438a56 100644 --- a/cli/src/commands/wheels/base.cfc +++ b/cli/src/commands/wheels/base.cfc @@ -997,6 +997,8 @@ component extends="wheels-cli.models.BaseCommand" excludeFromHelp=true { // Connect to information_schema for MySQL system operations return "jdbc:mysql://#local.host#:#local.port#/information_schema"; case "PostgreSQL": + case "Postgres": + case "Postgre": if (!Len(local.port)) local.port = "5432"; // Connect to postgres system database return "jdbc:postgresql://#local.host#:#local.port#/postgres"; @@ -1341,7 +1343,7 @@ component extends="wheels-cli.models.BaseCommand" excludeFromHelp=true { local.username = local.dbConfig.tempDS.username ?: ""; local.password = local.dbConfig.tempDS.password ?: ""; - printStep("Connecting to " & arguments.dbType & " database..."); + detailOutput.output("Connecting to " & arguments.dbType & " database..."); // Try to load driver local.driver = ""; @@ -1352,7 +1354,7 @@ component extends="wheels-cli.models.BaseCommand" excludeFromHelp=true { local.driver = createObject("java", local.driverClass); local.result.driverClass = local.driverClass; local.driverFound = true; - printSuccess("Driver found: " & local.driverClass); + detailOutput.statusSuccess("Driver found: " & local.driverClass); break; } catch (any driverError) { // Continue trying other drivers @@ -1376,8 +1378,6 @@ component extends="wheels-cli.models.BaseCommand" excludeFromHelp=true { } // Connect using driver directly - print.line(local.url); - print.redLine(local.props); local.conn = local.driver.connect(local.url, local.props); if (isNull(local.conn)) { @@ -1387,7 +1387,7 @@ component extends="wheels-cli.models.BaseCommand" excludeFromHelp=true { local.result.success = true; local.result.connection = local.conn; - printSuccess("Connected successfully to " & arguments.dbType & " database!"); + detailOutput.statusSuccess("Connected successfully to " & arguments.dbType & " database!"); return local.result; } catch (any e) { diff --git a/cli/src/commands/wheels/db/create.cfc b/cli/src/commands/wheels/db/create.cfc index f57c11a9c..6bc4f38a2 100644 --- a/cli/src/commands/wheels/db/create.cfc +++ b/cli/src/commands/wheels/db/create.cfc @@ -160,6 +160,8 @@ component extends="../base" { case "MySQL5": createDatabase(local.dsInfo, local.dbName, arguments.force, "MySQL"); break; + case "Postgre": + case "Postgres": case "PostgreSQL": createDatabase(local.dsInfo, local.dbName, arguments.force, "PostgreSQL"); break; @@ -246,7 +248,7 @@ component extends="../base" { // Test if driver accepts the URL if (!local.driver.acceptsURL(local.url)) { - detailOutput.error(arguments.dbType & " driver does not accept the URL format"); + detailOutput.error(arguments.dbType & " driver does not accept the URL format: #local.url#"); return; } @@ -579,6 +581,8 @@ component extends="../base" { local.templateKey = "mysql"; break; case "PostgreSQL": + case "Postgre": + case "Postgres": local.templateKey = "postgre"; break; case "MSSQLServer": @@ -874,6 +878,8 @@ component extends="../base" { case "MySQL": return "jdbc:mysql://#arguments.host#:#arguments.port#/#arguments.database#?characterEncoding=UTF-8&serverTimezone=UTC&maxReconnects=3"; case "PostgreSQL": + case "Postgre": + case "Postgres": return "jdbc:postgresql://#arguments.host#:#arguments.port#/#arguments.database#"; case "MSSQLServer": return "jdbc:sqlserver://#arguments.host#:#arguments.port#;DATABASENAME=#arguments.database#;trustServerCertificate=true;SelectMethod=direct"; @@ -1241,6 +1247,8 @@ component extends="../base" { case "MySQL": case "MySQL5": return "mysql"; + case "Postgre": + case "Postgres": case "PostgreSQL": return "postgres"; case "MSSQLServer": diff --git a/cli/src/commands/wheels/db/dump.cfc b/cli/src/commands/wheels/db/dump.cfc index 9d2ccaeac..fd5439aa3 100644 --- a/cli/src/commands/wheels/db/dump.cfc +++ b/cli/src/commands/wheels/db/dump.cfc @@ -20,8 +20,10 @@ component extends="../base" { * @dataOnly Dump only the data (no schema) * @tables Comma-separated list of tables to dump (defaults to all) * @compress Compress the output file using gzip - * @help Export database schema and data */ + + property name="detailOutput" inject="detailOutputService@wheels-cli"; + public void function run( string output = "", string datasource = "", @@ -32,17 +34,13 @@ component extends="../base" { string tables = "", boolean compress = false ) { - arguments = reconstructArgs(arguments); local.appPath = getCWD(); - - if (!isWheelsApp(local.appPath)) { - error("This command must be run from a Wheels application directory"); - return; - } + requireWheelsApp(local.appPath); + arguments = reconstructArgs(arguments); // Validate options if (arguments.schemaOnly && arguments.dataOnly) { - error("Cannot use both schemaOnly=true and dataOnly=true flags"); + detailOutput.error("Cannot use both schemaOnly=true and dataOnly=true flags"); return; } @@ -62,15 +60,15 @@ component extends="../base" { return; } - printHeader("Database Export Process"); - printInfo("Datasource", arguments.datasource); - printInfo("Environment", arguments.environment); + detailOutput.header("Database Export Process"); + detailOutput.statusInfo("Datasource: #arguments.datasource#"); + detailOutput.statusInfo("Environment: #arguments.environment#"); // Get datasource configuration - local.dsInfo = getDatasourceInfo(arguments.datasource); - + local.dsInfo = getDatasourceInfo(arguments.datasource, arguments.environment); + if (StructIsEmpty(local.dsInfo)) { - error("Datasource '" & arguments.datasource & "' not found in server configuration"); + detailOutput.error("Datasource '" & arguments.datasource & "' not found in server configuration"); return; } @@ -87,24 +85,24 @@ component extends="../base" { } // If no database found, show available databases else { - printDivider(); - printWarning("No database specified in datasource configuration"); - printStep("Fetching available databases..."); + detailOutput.divider(); + detailOutput.statusWarning("No database specified in datasource configuration"); + detailOutput.output("Fetching available databases..."); // Call getAvailableDatabases from base.cfc local.databases = getAvailableDatabases(local.dsInfo); if (ArrayLen(local.databases) == 0) { - error("No databases found or unable to connect to server"); + detailOutput.error("No databases found or unable to connect to server"); return; } - print.line(); - print.boldYellowLine("Available databases:"); + detailOutput.line(); + detailOutput.output("Available databases:"); for (local.i = 1; local.i <= ArrayLen(local.databases); local.i++) { - print.line(" #local.i#. #local.databases[local.i]#"); + detailOutput.output("#local.i#. #local.databases[local.i]#", true); } - print.line(); + detailOutput.line(); // Ask user to select local.selection = ask("Select database number (or type database name): "); @@ -117,7 +115,7 @@ component extends="../base" { else if (Len(Trim(local.selection))) { local.selectedDatabase = Trim(local.selection); } else { - error("No database selected"); + detailOutput.error("No database selected"); return; } } @@ -128,47 +126,50 @@ component extends="../base" { // Generate output filename if not provided if (!Len(arguments.output)) { local.timestamp = DateFormat(Now(), "yyyymmdd") & TimeFormat(Now(), "HHmmss"); - arguments.output = "dump_" & local.selectedDatabase & "_" & local.timestamp & ".sql"; + arguments.output = fileSystemUtil.resolvePath("dump_" & local.selectedDatabase & "_" & local.timestamp & ".sql"); if (arguments.compress) { arguments.output &= ".gz"; } } // Make sure output path is absolute - if (!FileExists(GetDirectoryFromPath(arguments.output)) && !DirectoryExists(GetDirectoryFromPath(arguments.output))) { + if (!FileExists((arguments.output)) && !DirectoryExists(fileSystemUtil.resolvePath(arguments.output))) { // If no directory specified, use current directory - if (GetDirectoryFromPath(arguments.output) == "") { + if (fileSystemUtil.resolvePath(arguments.output) == "") { arguments.output = local.appPath & "/" & arguments.output; } } + - printInfo("Database", local.selectedDatabase); - printInfo("Output File", arguments.output); + detailOutput.statusInfo("Database: #local.selectedDatabase#"); + detailOutput.statusInfo("Output File: #arguments.output#"); if (arguments.schemaOnly) { - printInfo("Mode", "Schema only"); + detailOutput.statusInfo("Mode: Schema only"); } else if (arguments.dataOnly) { - printInfo("Mode", "Data only"); + detailOutput.statusInfo("Mode: Data only"); } else { - printInfo("Mode", "Schema and data"); + detailOutput.statusInfo("Mode: Schema and data"); } if (Len(arguments.tables)) { - printInfo("Tables", arguments.tables); + detailOutput.statusInfo("Tables: #arguments.tables#"); } if (arguments.compress) { - printInfo("Compression", "Enabled"); + detailOutput.statusInfo("Compression: Enabled"); } - printDivider(); + detailOutput.divider(); // Display database connection info local.dbType = local.dsInfo.driver; - printInfo("Database Type", local.dbType); - printInfo("Host", local.dsInfo.host ?: "localhost"); - printInfo("Port", local.dsInfo.port ?: "default"); - printDivider(); + local.dsInfo.host = local.dsInfo.host ?: "localhost"; + local.dsInfo.port = local.dsInfo.port ?: "default"; + detailOutput.statusInfo("Database Type: #local.dbType#"); + detailOutput.statusInfo("Host: #local.dsInfo.host#"); + detailOutput.statusInfo("Port: #local.dsInfo.port#"); + detailOutput.divider(); // Execute dump based on database type local.success = false; @@ -179,6 +180,8 @@ component extends="../base" { local.success = dumpMySQL(local.dsInfo, arguments); break; case "PostgreSQL": + case "Postgres": + case "Postgre": local.success = dumpPostgreSQL(local.dsInfo, arguments); break; case "MSSQLServer": @@ -189,35 +192,36 @@ component extends="../base" { local.success = dumpH2(local.dsInfo, arguments); break; default: - error("Database dump not supported for driver: " & local.dbType); - print.line("Please use your database management tools to export the database."); + detailOutput.error("Database dump not supported for driver: " & local.dbType); + detailOutput.statusInfo("Please use your database management tools to export the database."); + return; } if (local.success) { - printDivider(); - printSuccess("Database exported successfully!"); - printInfo("Output File", arguments.output); + detailOutput.divider(); + detailOutput.statusSuccess("Database exported successfully!"); + detailOutput.statusInfo("Output File: #arguments.output#"); // Show file size if (FileExists(arguments.output)) { try { local.fileInfo = GetFileInfo(arguments.output); local.sizeInMB = NumberFormat(local.fileInfo.size / 1048576, "0.00"); - printInfo("File Size", local.sizeInMB & " MB"); + detailOutput.statusInfo("File Size: #local.sizeInMB# MB"); } catch (any e) { // Ignore file info errors } } - systemOutput("", true, true); - printSuccess("Export completed successfully!", true); + detailOutput.statusSuccess("Export completed successfully!"); } } catch (any e) { - printError("Error exporting database: " & e.message); + detailOutput.error("Error exporting database: #e.message#"); if (StructKeyExists(e, "detail") && Len(e.detail)) { - printError("Details: " & e.detail); + detailOutput.error("Details: #e.detail#"); } + return; } } @@ -232,11 +236,11 @@ component extends="../base" { // Check if database name is provided (should always have one now due to selection logic) if (!Len(local.database)) { - printError("No database name specified"); + detailOutput.error("No database name specified"); return false; } - printStep("Preparing MySQL dump for database: " & local.database); + detailOutput.output("Preparing MySQL dump for database: #local.database#"); // Try mysqldump first local.mysqldumpSuccess = false; @@ -263,14 +267,14 @@ component extends="../base" { // Found mysqldump, use full path local.checkResult.success = true; local.mysqldumpPath = local.path; - printInfo("Found mysqldump at:", local.path); + detailOutput.statusInfo("Found mysqldump at:", local.path); break; } } } if (local.checkResult.success) { - printStep("Found mysqldump, attempting native dump..."); + detailOutput.output("Found mysqldump, attempting native dump..."); local.mysqldumpAttempted = true; // Build mysqldump command - use full path if found @@ -319,7 +323,7 @@ component extends="../base" { if (!local.result.success && FindNoCase("caching_sha2_password", local.result.error)) { // Try with mysql_native_password - printWarning("Authentication plugin issue detected, trying compatibility mode..."); + detailOutput.statusWarning("Authentication plugin issue detected, trying compatibility mode..."); local.cmd = Replace(local.cmd, "mysqldump", "mysqldump --default-auth=mysql_native_password"); local.result = executeSystemCommand(local.cmd, local.envVars); } @@ -327,37 +331,37 @@ component extends="../base" { local.mysqldumpSuccess = local.result.success; if (!local.mysqldumpSuccess) { - printWarning("mysqldump failed: " & (local.result.error ?: "Unknown error")); + detailOutput.statusWarning("mysqldump failed: " & (local.result.error ?: "Unknown error")); } } // If mysqldump failed or wasn't found, use JDBC fallback if (!local.mysqldumpSuccess) { if (local.mysqldumpAttempted) { - printStep("Falling back to JDBC-based export..."); + detailOutput.output("Falling back to JDBC-based export..."); } else { - printStep("mysqldump not found, using JDBC-based export..."); + detailOutput.output("mysqldump not found, using JDBC-based export..."); } - + return dumpMySQLViaJDBC(arguments.dsInfo, arguments.options); } // Handle compression for Windows if needed if (arguments.options.compress && isWindows() && FileExists(arguments.options.output)) { - printWarning("Note: Compression not available on Windows with mysqldump"); + detailOutput.statusWarning("Note: Compression not available on Windows with mysqldump"); } - printSuccess("MySQL dump completed successfully using mysqldump"); + detailOutput.statusSuccess("MySQL dump completed successfully using mysqldump"); return true; } catch (any e) { - printError("MySQL dump error: " & e.message); + detailOutput.error("MySQL dump error: " & e.message); // Try JDBC as last resort - printStep("Attempting JDBC fallback due to error..."); + detailOutput.output("Attempting JDBC fallback due to error..."); try { return dumpMySQLViaJDBC(arguments.dsInfo, arguments.options); } catch (any jdbcError) { - printError("JDBC fallback also failed: " & jdbcError.message); + detailOutput.statusFailed("JDBC fallback also failed: " & jdbcError.message); return false; } } @@ -365,28 +369,28 @@ component extends="../base" { private boolean function dumpMySQLViaJDBC(required struct dsInfo, required struct options) { try { - systemOutput("Using JDBC connection for database export"); - printWarning("Note: JDBC export may not include stored procedures, triggers, or views"); + detailOutput.output("Using JDBC connection for database export"); + detailOutput.statusWarning("Note: JDBC export may not include stored procedures, triggers, or views"); // Get database connection local.connResult = getDatabaseConnection(arguments.dsInfo, "MySQL"); if (!local.connResult.success) { - printError("Failed to connect to MySQL database via JDBC"); + detailOutput.error("Failed to connect to MySQL database via JDBC"); if (Len(local.connResult.error)) { // Check for specific error types if (FindNoCase("Communications link failure", local.connResult.error)) { - printError("Connection failed - MySQL server may not be running"); - print.line(); - printWarning("Please check:"); - print.line("1. MySQL is running on port " & (arguments.dsInfo.port ?: "3306")); - print.line("2. Firewall is not blocking the connection"); - print.line("3. Host '" & (arguments.dsInfo.host ?: "localhost") & "' is correct"); + detailOutput.statusFailed("Connection failed - MySQL server may not be running"); + detailOutput.output(""); + detailOutput.statusWarning("Please check:"); + detailOutput.output("1. MySQL is running on port " & (arguments.dsInfo.port ?: "3306")); + detailOutput.output("2. Firewall is not blocking the connection"); + detailOutput.output("3. Host '" & (arguments.dsInfo.host ?: "localhost") & "' is correct"); } else if (FindNoCase("Access denied", local.connResult.error)) { - printError("Authentication failed"); - print.line("Please verify username and password are correct"); + detailOutput.statusFailed("Authentication failed"); + detailOutput.output("Please verify username and password are correct"); } else { - printError("Error: " & local.connResult.error); + detailOutput.error("Error: " & local.connResult.error); } } return false; @@ -424,7 +428,7 @@ component extends="../base" { local.stmt.close(); } - printInfo("Tables to export:", ArrayLen(local.tableList)); + detailOutput.statusInfo("Tables to export: " & ArrayLen(local.tableList)); // Track progress local.tableCount = 0; @@ -433,7 +437,7 @@ component extends="../base" { // Process each table for (local.table in local.tableList) { local.tableCount++; - printProgress("Exporting table " & local.tableCount & "/" & ArrayLen(local.tableList) & ": " & local.table); + detailOutput.statusInfo("Exporting table " & local.tableCount & "/" & ArrayLen(local.tableList) & ": " & local.table); if (!arguments.options.dataOnly) { // Get CREATE TABLE statement @@ -549,13 +553,13 @@ component extends="../base" { // Handle compression if requested (basic zip on Windows) if (arguments.options.compress && isWindows()) { - printStep("Compressing output file..."); + detailOutput.output("Compressing output file..."); // This is a basic implementation - could be enhanced - printWarning("Compression support is limited in JDBC mode"); + detailOutput.statusWarning("Compression support is limited in JDBC mode"); } - print.greenLine("MySQL dump completed successfully via JDBC"); - print.yellowLine("Exported:", ArrayLen(local.tableList) & " tables, " & local.totalRows & " rows"); + detailOutput.statusSuccess("MySQL dump completed successfully via JDBC"); + detailOutput.statusInfo("Exported: " & ArrayLen(local.tableList) & " tables, " & local.totalRows & " rows"); return true; @@ -566,22 +570,13 @@ component extends="../base" { } } catch (any e) { - printError("MySQL JDBC dump error: " & e.message); + detailOutput.error("MySQL JDBC dump error: " & e.message); if (StructKeyExists(e, "detail")) { - printError("Detail: " & e.detail); + detailOutput.error("Detail: " & e.detail); } return false; } } - - private void function printProgress(required string message) { - // Use carriage return for progress updates on same line - if (isWindows()) { - systemOutput(Chr(13) & " ... " & arguments.message & " ", false, false); - } else { - systemOutput(" ... " & arguments.message, true, true); - } - } private boolean function dumpPostgreSQL(required struct dsInfo, required struct options) { try { @@ -591,20 +586,135 @@ component extends="../base" { local.database = arguments.dsInfo.database; local.username = arguments.dsInfo.username ?: ""; - // Check if database name is provided (should always have one now due to selection logic) + // Check if database name is provided if (!Len(local.database)) { - printError("No database name specified"); + detailOutput.error("No database name specified"); return false; } - printStep("Preparing PostgreSQL dump for database: " & local.database); + detailOutput.statusInfo("Preparing PostgreSQL dump for database: " & local.database); + + // Check if pg_dump is available FIRST + local.checkCmd = isWindows() ? "where pg_dump" : "which pg_dump"; + local.checkResult = executeSystemCommand(local.checkCmd); + + if (!local.checkResult.success) { + // pg_dump not found - provide installation guide and exit + detailOutput.line(); + detailOutput.error("PostgreSQL client tools (pg_dump) not found!"); + detailOutput.line(); + detailOutput.statusWarning("pg_dump is required to export PostgreSQL databases"); + detailOutput.line(); + + detailOutput.output("Installation Guide:"); + detailOutput.line(); + + // Detect OS and provide specific instructions + if (isWindows()) { + detailOutput.output("For Windows:"); + detailOutput.output("1. Download PostgreSQL from: https://www.postgresql.org/download/windows/"); + detailOutput.output("2. Run the installer (you can choose 'Command Line Tools only' if you don't need the full server)"); + detailOutput.output("3. Add PostgreSQL bin directory to your PATH:"); + detailOutput.output("- Default location: C:\Program Files\PostgreSQL\[version]\bin", true); + detailOutput.output("- Add to PATH: System Properties → Environment Variables → Path", true); + detailOutput.output("4. Restart your terminal/CommandBox"); + detailOutput.line(); + detailOutput.output("Alternative - Using Chocolatey:"); + detailOutput.output("choco install postgresql", true); + } else if (isMac()) { + detailOutput.output("For macOS:"); + detailOutput.line(); + detailOutput.output("Option 1 - Using Homebrew (recommended):"); + detailOutput.output("brew install postgresql", true); + detailOutput.line(); + detailOutput.output("Option 2 - PostgreSQL.app:"); + detailOutput.output(" Download from: https://postgresapp.com/"); + detailOutput.output(" After installation, add to PATH:"); + detailOutput.output(" export PATH=$PATH:/Applications/Postgres.app/Contents/Versions/latest/bin"); + detailOutput.line(); + detailOutput.output("Option 3 - Using MacPorts:"); + detailOutput.output(" sudo port install postgresql16 +universal"); + } else { + // Assume Linux/Unix + detailOutput.output("For Linux:"); + detailOutput.line(); + detailOutput.output("Ubuntu/Debian:"); + detailOutput.output(" sudo apt-get update"); + detailOutput.output(" sudo apt-get install postgresql-client"); + detailOutput.line(); + detailOutput.output("RHEL/CentOS/Fedora:"); + detailOutput.output(" sudo yum install postgresql"); + detailOutput.output(" ## or for newer versions:"); + detailOutput.output(" sudo dnf install postgresql"); + detailOutput.line(); + detailOutput.output("Arch Linux:"); + detailOutput.output(" sudo pacman -S postgresql"); + detailOutput.line(); + detailOutput.output("Alpine Linux:"); + detailOutput.output(" apk add postgresql-client"); + } + + detailOutput.line(); + detailOutput.output("After installation:"); + detailOutput.output("1. Verify installation: pg_dump --version", true); + detailOutput.output("2. Restart CommandBox", true); + detailOutput.output("3. Try the export again", true); + detailOutput.line(); + + return false; // Exit - no pg_dump, no export + } + + // pg_dump is available, proceed with dump + detailOutput.statusSuccess("Found pg_dump, proceeding with database export..."); + + // Ensure output directory exists + local.outputFile = ExpandPath(arguments.options.output); + if (arguments.options.output contains ":" || Left(arguments.options.output, 1) == "/" || Left(arguments.options.output, 1) == "\") { + local.outputFile = arguments.options.output; + } + + // Convert to absolute path and normalize + local.outputFile = FileSystemUtil.resolvePath(local.outputFile); + + // For Windows: Fix the output file path + if (isWindows()) { + // Convert to forward slashes to avoid escaping issues + local.outputFile = Replace(local.outputFile, "\", "/", "all"); + // Remove trailing slash if present + if (Right(local.outputFile, 1) == "/") { + local.outputFile = Left(local.outputFile, Len(local.outputFile)-1); + } + } + + local.outputDir = GetDirectoryFromPath(local.outputFile); + if (Len(local.outputDir) && !DirectoryExists(local.outputDir)) { + DirectoryCreate(local.outputDir, true); + } + + // Check if a folder with the .sql name exists and remove it + if (DirectoryExists(local.outputFile)) { + detailOutput.statusWarning("Found directory with same name as output file: " & local.outputFile); + try { + // Try to remove the directory if it's empty + local.dirList = DirectoryList(local.outputFile, false, "name"); + if (ArrayLen(local.dirList) == 0) { + DirectoryDelete(local.outputFile, false); + detailOutput.statusInfo("Removed empty directory that was blocking file creation"); + } else { + detailOutput.error("Directory is not empty, cannot remove. Please manually delete: " & local.outputFile); + return false; + } + } catch(any e) { + detailOutput.error("Could not remove directory: " & e.message); + } + } - // Build pg_dump command + // Build pg_dump command WITHOUT quotes for Windows local.cmd = "pg_dump"; - local.cmd &= " -h " & local.host; + local.cmd &= " -h " & local.host; // No quotes local.cmd &= " -p " & local.port; - local.cmd &= " -U " & local.username; - local.cmd &= " -d " & local.database; + local.cmd &= " -U " & local.username; // No quotes + local.cmd &= " -d " & local.database; // No quotes local.cmd &= " --verbose"; // Show progress // Add options @@ -618,7 +728,7 @@ component extends="../base" { if (Len(arguments.options.tables)) { local.tableList = ListToArray(arguments.options.tables); for (local.table in local.tableList) { - local.cmd &= " -t " & Trim(local.table); + local.cmd &= " -t " & Trim(local.table); // No quotes } } @@ -627,7 +737,14 @@ component extends="../base" { local.cmd &= " -Z 9"; // Maximum compression } - local.cmd &= " -f " & Chr(34) & arguments.options.output & Chr(34); + // For Windows: Use shell redirection instead of -f option + if (isWindows()) { + // Use stdout redirection instead of -f parameter + local.cmd &= " > " & Chr(34) & local.outputFile & Chr(34); + } else { + // Unix/Linux: Use -f parameter + local.cmd &= " -f " & Chr(34) & local.outputFile & Chr(34); + } // Set PGPASSWORD environment variable if provided local.envVars = {}; @@ -635,136 +752,173 @@ component extends="../base" { local.envVars["PGPASSWORD"] = arguments.dsInfo.password; } - printStep("Executing PostgreSQL dump..."); - printInfo("Command", "pg_dump (output to file)"); + detailOutput.output("Executing PostgreSQL dump..."); + detailOutput.statusInfo("Command: pg_dump (output to file)"); + detailOutput.statusInfo("Output file: " & local.outputFile); local.result = executeSystemCommand(local.cmd, local.envVars); if (local.result.success) { - printSuccess("PostgreSQL dump completed successfully"); + detailOutput.statusSuccess("PostgreSQL dump completed successfully"); + + // Check if file was created and has content + if (FileExists(local.outputFile)) { + try { + local.fileInfo = CreateObject("java", "java.io.File").init(local.outputFile); + local.fileSize = local.fileInfo.length(); + if (local.fileSize > 0) { + detailOutput.statusSuccess("Export file size: " & NumberFormat(local.fileSize / 1024, "0.00") & " KB"); + } else { + detailOutput.statusWarning("Export file is empty - check database permissions"); + } + } catch (any e) { + // Just note that file was created + detailOutput.statusSuccess("Export file created: " & local.outputFile); + } + } else { + detailOutput.error("Export file was not created: " & local.outputFile); + return false; + } } else { - printError("PostgreSQL dump failed"); + detailOutput.error("PostgreSQL dump failed"); if (StructKeyExists(local.result, "output") && Len(Trim(local.result.output))) { - printError("Output: " & local.result.output); + detailOutput.error("Output: " & local.result.output); } if (StructKeyExists(local.result, "error") && Len(local.result.error)) { - printError("Error: " & local.result.error); + detailOutput.error("Error: " & local.result.error); } - print.line(); - printWarning("Troubleshooting tips:"); - print.line("1. Ensure pg_dump is installed and in your PATH"); - print.line("2. On Windows: Install PostgreSQL"); - print.line("3. On Mac: brew install postgresql"); - print.line("4. On Linux: sudo apt-get install postgresql-client"); + + detailOutput.line(); + detailOutput.statusInfo("Troubleshooting tips:"); + detailOutput.output("1. Check database connection settings", true); + detailOutput.output("2. Verify user has necessary permissions", true); + detailOutput.output("3. Ensure database name is correct", true); + detailOutput.output("4. Check if PostgreSQL server is running", true); + detailOutput.output("5. Try connecting with psql first to test credentials", true); } return local.result.success; } catch (any e) { - printError("PostgreSQL dump error: " & e.message); + detailOutput.error("PostgreSQL dump error: " & e.message); + if (StructKeyExists(e, "detail")) { + detailOutput.error("Details: " & e.detail); + } return false; } } private boolean function dumpSQLServer(required struct dsInfo, required struct options) { try { - printStep("Preparing SQL Server export..."); - - // Check if sqlcmd is available - local.checkCmd = isWindows() ? "where sqlcmd" : "which sqlcmd"; - local.checkResult = executeSystemCommand(local.checkCmd); + detailOutput.output("Preparing SQL Server export..."); + + /* ---------------------------------------------------- + 1. Get database connection details + ---------------------------------------------------- */ + local.host = dsInfo.host ?: "localhost"; + local.port = dsInfo.port ?: "1433"; + local.database = dsInfo.database; + local.username = dsInfo.username ?: ""; + local.password = dsInfo.password ?: ""; - if (!local.checkResult.success) { - printWarning("sqlcmd command not found"); - printWarning("Generating basic SQL script instead of full backup"); - - // Generate a basic SQL script - local.output = ""; - local.output &= "-- SQL Server Database Export" & Chr(10); - local.output &= "-- Generated: " & DateTimeFormat(Now(), "yyyy-mm-dd HH:nn:ss") & Chr(10); - local.output &= "-- Database: " & arguments.dsInfo.database & Chr(10); - local.output &= "-- Server: " & (arguments.dsInfo.host ?: "localhost") & Chr(10); - local.output &= Chr(10); - local.output &= "-- WARNING: This is a basic export template." & Chr(10); - local.output &= "-- For complete backup, use SQL Server Management Studio (SSMS)" & Chr(10); - local.output &= "-- or SQL Server Data Tools (SSDT)" & Chr(10); - local.output &= Chr(10); - local.output &= "USE [" & arguments.dsInfo.database & "];" & Chr(10); - local.output &= "GO" & Chr(10); - local.output &= Chr(10); - - // Add basic structure comments - if (!arguments.options.dataOnly) { - local.output &= "-- To generate full schema script in SSMS:" & Chr(10); - local.output &= "-- 1. Right-click database -> Tasks -> Generate Scripts" & Chr(10); - local.output &= "-- 2. Select objects to script" & Chr(10); - local.output &= "-- 3. Set scripting options" & Chr(10); - local.output &= "-- 4. Save to file" & Chr(10); - } - - FileWrite(arguments.options.output, local.output); - printSuccess("Basic SQL Server export template created"); - printWarning("Use SSMS for complete database export"); - - return true; + if (!Len(local.database)) { + detailOutput.error("No database name specified"); + return false; } + + detailOutput.statusInfo("Database: #local.database#"); + detailOutput.statusInfo("Server: #local.host#:#local.port#"); + + /* ---------------------------------------------------- + 2. Generate backup file path + ---------------------------------------------------- */ + local.outputFile = arguments.options.output; - // If sqlcmd is available, use it - local.host = arguments.dsInfo.host ?: "localhost"; - local.port = arguments.dsInfo.port ?: "1433"; - local.database = arguments.dsInfo.database; - local.username = arguments.dsInfo.username ?: ""; - local.password = arguments.dsInfo.password ?: ""; - - // Build sqlcmd command for generating scripts - local.cmd = "sqlcmd"; - local.cmd &= " -S " & local.host & "," & local.port; - local.cmd &= " -d " & local.database; - - if (Len(local.username)) { - local.cmd &= " -U " & local.username; - if (Len(local.password)) { - local.cmd &= " -P " & local.password; - } + // If no output file specified, generate default .bak filename + if (!Len(local.outputFile)) { + local.timestamp = DateFormat(Now(), "yyyymmdd") & TimeFormat(Now(), "HHmmss"); + local.outputFile = fileSystemUtil.resolvePath("backup_" & local.database & "_" & local.timestamp & ".bak"); } else { - local.cmd &= " -E"; // Windows authentication + // Ensure it has .bak extension + if (ListLast(local.outputFile, ".") != "bak") { + local.outputFile &= ".bak"; + } + local.outputFile = fileSystemUtil.resolvePath(local.outputFile); } - // Create a script to generate DDL - local.scriptFile = GetTempDirectory() & "export_script.sql"; - local.script = "-- Export script commands here"; - FileWrite(local.scriptFile, local.script); - - local.cmd &= " -i " & Chr(34) & local.scriptFile & Chr(34); - local.cmd &= " -o " & Chr(34) & arguments.options.output & Chr(34); - - printStep("Executing SQL Server export..."); - local.result = executeSystemCommand(local.cmd); - - // Clean up temp file - if (FileExists(local.scriptFile)) { - FileDelete(local.scriptFile); + // Ensure directory exists + local.outputDir = GetDirectoryFromPath(local.outputFile); + if (Len(local.outputDir) && !DirectoryExists(local.outputDir)) { + DirectoryCreate(local.outputDir, true); + detailOutput.statusInfo("Created directory: #local.outputDir#"); + } + + detailOutput.statusInfo("Backup file: #local.outputFile#"); + + /* ---------------------------------------------------- + 3. Try using sqlcmd (Windows) + ---------------------------------------------------- */ + local.useSqlCmd = false; + local.sqlCmdResult = ""; + + if (isWindows()) { + detailOutput.output("Checking for sqlcmd..."); + local.checkCmd = "where sqlcmd"; + local.checkResult = executeSystemCommand(local.checkCmd); + + if (local.checkResult.success) { + detailOutput.statusSuccess("Found sqlcmd, using native backup"); + local.useSqlCmd = true; + + // Convert path for Windows + local.backupPath = Replace(local.outputFile, "/", "\", "all"); + local.backupPath = Replace(local.backupPath, "\\", "\", "all"); + + // Build sqlcmd command + local.cmd = 'sqlcmd'; + + // Add authentication + if (Len(local.username) && Len(local.password)) { + local.cmd &= ' -S "#local.host#,#local.port#" -U "#local.username#" -P "#local.password#"'; + } else { + local.cmd &= ' -S "#local.host#,#local.port#" -E'; // Windows Authentication + } + + // Add backup command + local.cmd &= ' -Q "BACKUP DATABASE [#local.database#] TO DISK=''#local.backupPath#'' WITH INIT, FORMAT, COMPRESSION, STATS=5"'; + + detailOutput.statusInfo("Executing SQL Server backup..."); + local.sqlCmdResult = executeSystemCommand(local.cmd); + + if (local.sqlCmdResult.success) { + detailOutput.statusSuccess("SQL Server backup completed successfully via sqlcmd"); + return true; + } else { + detailOutput.statusFailed("#local.sqlCmdResult.error#"); + return; + } + } else { + detailOutput.statusWarning("sqlcmd not found"); + } } - - return local.result.success; } catch (any e) { - printError("SQL Server export error: " & e.message); + detailOutput.error("SQL Server export error: " & e.message); return false; } } private boolean function dumpH2(required struct dsInfo, required struct options) { try { - printStep("Preparing H2 database export..."); + detailOutput.output("Preparing H2 database export..."); // Get database connection local.connResult = getDatabaseConnection(arguments.dsInfo, "H2"); if (!local.connResult.success) { - printError("Failed to connect to H2 database"); + detailOutput.error("Failed to connect to H2 database"); if (Len(local.connResult.error)) { - printError("Error: " & local.connResult.error); + detailOutput.error(local.connResult.error); } return false; } @@ -772,7 +926,7 @@ component extends="../base" { local.conn = local.connResult.connection; try { - printStep("Generating H2 database script..."); + detailOutput.output("Generating H2 database script..."); local.stmt = local.conn.createStatement(); local.sql = "SCRIPT"; @@ -803,11 +957,11 @@ component extends="../base" { local.stmt.execute(local.sql); local.stmt.close(); - printSuccess("H2 database exported successfully"); + detailOutput.statusSuccess("H2 database exported successfully"); return true; } catch (any e) { - printError("H2 export error: " & e.message); + detailOutput.error("H2 export error: " & e.message); return false; } finally { if (IsDefined("local.conn")) { @@ -816,48 +970,60 @@ component extends="../base" { } } catch (any e) { - printError("H2 database export error: " & e.message); + detailOutput.error("H2 database export error: " & e.message); return false; } } private struct function executeSystemCommand(required string command, struct envVars = {}) { - try { - // Use CommandBox's native command execution - local.runtime = CreateObject("java", "java.lang.Runtime").getRuntime(); - local.isWin = isWindows(); - - // Build the command array - if (local.isWin) { - // Windows command execution - local.fullCommand = "cmd /c " & arguments.command; - } else { - // Unix/Linux/Mac command execution - local.fullCommand = arguments.command; + local.runtime = CreateObject("java", "java.lang.Runtime").getRuntime(); + local.isWin = isWindows(); + + + // Build the command array + if (local.isWin) { + // For Windows: Use array format to avoid quoting issues + // Split the command into an array of arguments + local.cmdArray = ["cmd", "/c", arguments.command]; + local.fullCommand = local.cmdArray; + } else { + // Unix/Linux/Mac: Use string command + local.fullCommand = arguments.command; + } + + // Set up environment + local.envArray = []; + if (!StructIsEmpty(arguments.envVars)) { + // Get current environment + local.currentEnv = CreateObject("java", "java.lang.System").getenv(); + + // Convert to array format + for (local.key in local.currentEnv) { + ArrayAppend(local.envArray, local.key & "=" & local.currentEnv[local.key]); } - // Set up environment - local.envArray = []; - if (!StructIsEmpty(arguments.envVars)) { - // Get current environment - local.currentEnv = CreateObject("java", "java.lang.System").getenv(); - - // Convert to array format - for (local.key in local.currentEnv) { - ArrayAppend(local.envArray, local.key & "=" & local.currentEnv[local.key]); - } - - // Add/override with our variables - for (local.key in arguments.envVars) { - ArrayAppend(local.envArray, local.key & "=" & arguments.envVars[local.key]); - } + // Add/override with our variables + for (local.key in arguments.envVars) { + ArrayAppend(local.envArray, local.key & "=" & arguments.envVars[local.key]); } - - // Execute the command + } + + // Execute the command + try { if (ArrayLen(local.envArray)) { - local.process = local.runtime.exec(local.fullCommand, local.envArray); + if (local.isWin && IsArray(local.fullCommand)) { + // Use array format for Windows + local.process = local.runtime.exec(local.fullCommand, local.envArray); + } else { + local.process = local.runtime.exec(local.fullCommand, local.envArray); + } } else { - local.process = local.runtime.exec(local.fullCommand); + if (local.isWin && IsArray(local.fullCommand)) { + // Use array format for Windows + local.process = local.runtime.exec(local.fullCommand); + } else { + local.process = local.runtime.exec(local.fullCommand); + } } // Wait for completion @@ -907,7 +1073,7 @@ component extends="../base" { } catch (any e) { return { success: false, - error: e.message, + error: e.message & " " & e.detail, exitCode: -1, output: "" }; @@ -918,5 +1084,10 @@ component extends="../base" { local.os = CreateObject("java", "java.lang.System").getProperty("os.name"); return FindNoCase("Windows", local.os) > 0; } - + + // Helper function to detect Mac OS + private boolean function isMac() { + local.os = CreateObject("java", "java.lang.System").getProperty("os.name"); + return FindNoCase("mac", local.os) > 0; + } } \ No newline at end of file diff --git a/cli/src/commands/wheels/init.cfc b/cli/src/commands/wheels/init.cfc index 6bc420803..b51dbb650 100644 --- a/cli/src/commands/wheels/init.cfc +++ b/cli/src/commands/wheels/init.cfc @@ -18,7 +18,7 @@ component extends="base" { * **/ function run() { - + requireWheelsApp(getCWD()); detailOutput.header("Wheels init") .output("This function will attempt to add a few things") @@ -66,7 +66,7 @@ component extends="base" { if(!fileExists(serverJsonLocation)){ var appName = ask( message = "Please enter an application name: we use this to make the server.json servername unique: ", defaultResponse = 'myapp'); appName = helpers.stripSpecialChars(appName); - var setEngine = ask( message = 'Please enter a default cfengine: ', defaultResponse = 'lucee6' ); + var setEngine = ask( message = 'Please enter a default cfengine: ', defaultResponse = 'lucee@6' ); // Make server.json server name unique to this app: assumes lucee by default detailOutput.statusInfo("Creating default server.json"); diff --git a/cli/src/commands/wheels/reload.cfc b/cli/src/commands/wheels/reload.cfc index 69c645297..abe432c89 100644 --- a/cli/src/commands/wheels/reload.cfc +++ b/cli/src/commands/wheels/reload.cfc @@ -21,6 +21,7 @@ component aliases='wheels r' extends="base" { property name="detailOutput" inject="DetailOutputService@wheels-cli"; function run(string mode="development", string password="") { + requireWheelsApp(getCWD()); arguments=reconstructArgs(arguments); var serverDetails = $getServerInfo(); diff --git a/cli/src/models/DetailOutputService.cfc b/cli/src/models/DetailOutputService.cfc index c3a6e2864..ab342085a 100644 --- a/cli/src/models/DetailOutputService.cfc +++ b/cli/src/models/DetailOutputService.cfc @@ -134,9 +134,9 @@ component { * @width The width of the header (default: 50) */ function header(required string title, numeric width = 50) { - print.line(repeatString("=", arguments.width)).toConsole(); + print.greenLine(repeatString("=", arguments.width)).toConsole(); print.boldLine(centerString(arguments.title, arguments.width)).toConsole(); - print.line(repeatString("=", arguments.width)).toConsole(); + print.greenLine(repeatString("=", arguments.width)).toConsole(); print.line().toConsole(); return this; } @@ -248,9 +248,23 @@ component { * @message The message to output * @indent Whether to indent this message */ - function output(required string message, boolean indent = false) { - var indentText = arguments.indent ? repeatString(" ", variables.indentSize) : ""; - print.line(indentText & arguments.message).toConsole(); + public function output( + required string message, + boolean indent = false + ) { + var prefix = arguments.indent ? + RepeatString(" ", variables.indentSize) : + ""; + + // Determine appropriate formatting based on indentation flag + if (arguments.indent) { + // Standard output for indented content (typically detailed info) + print.line(prefix & arguments.message).toConsole(); + } else { + // Highlighted output for main content + print.yellowLine(prefix & arguments.message).toConsole(); + } + return this; }