diff --git a/plugins.json b/plugins.json index 01feab3..024acb5 100644 --- a/plugins.json +++ b/plugins.json @@ -7,10 +7,9 @@ "zip": "https://docs.vincentdevelopment.ca/files/gravity-forms.zip", "activate": true }, - { "slug": "autodescription", - "activate": true + "activate": false }, { "slug": "better-search-replace", diff --git a/windows/bootstrap.bat b/windows/bootstrap.bat deleted file mode 100644 index e19984e..0000000 --- a/windows/bootstrap.bat +++ /dev/null @@ -1,4 +0,0 @@ -@echo off -REM One-click launcher for the Windows PowerShell bootstrap. -REM Usage: bootstrap "Client Site Name" -PowerShell -ExecutionPolicy Bypass -File "%~dp0wp-bootstrap.ps1" -ProjectName "%*" diff --git a/windows/wp-bootstrap.ps1 b/windows/wp-bootstrap.ps1 index 3993bb1..5cfc8ff 100644 --- a/windows/wp-bootstrap.ps1 +++ b/windows/wp-bootstrap.ps1 @@ -1,147 +1,318 @@ +# VDI WP Bootstrap — Windows (PowerShell + Herd) — local wp-cli.phar via php.exe Param( [Parameter(Mandatory=$true)][string]$ProjectName, - [string]$AdminUser = "vdidev", - [string]$AdminEmail = "dev@vincentdesign.ca", - [string]$ThemeStarterRepo = "https://github.com/vincent-design-inc/starter-theme-3.git", - [string]$HerdWorkspace = "$HOME\Herd", - [string]$LocalTld = "test", - [string]$MysqlHost = "127.0.0.1", - [string]$MysqlPort = "3306", - [string]$MysqlRootUser = "root", - [string]$MysqlRootPass = "", - # If WP-CLI isn't globally installed, set this to 'php .\wp-cli.phar' - [string]$WpCliPath = "wp" + [string]$AdminUser, + [string]$AdminEmail, + [string]$ThemeStarterRepo, + [string]$HerdWorkspace, + [string]$LocalTld, + [string]$MysqlHost, + [string]$MysqlPort, + [string]$MysqlRootUser, + [string]$MysqlRootPass ) -function Slugify([string]$s) { ($s.ToLower() -replace '[^a-z0-9]+','-').Trim('-') } +$ErrorActionPreference = 'Stop' +$env:WP_CLI_STRICT_ARGS_MODE = '1' -function RandPass() { - $bytes = New-Object 'System.Byte[]' 18 - (New-Object System.Security.Cryptography.RNGCryptoServiceProvider).GetBytes($bytes) - [Convert]::ToBase64String($bytes) -replace '[^a-zA-Z0-9]','' | ForEach-Object { $_.Substring(0,24) } +# ---- Load rc (CLI > rc > hard default) ---- +$RcFile = Join-Path $HOME ".wp-bootstraprc" +$Rc = @{} +if (Test-Path $RcFile) { + Get-Content $RcFile | ForEach-Object { + $line = $_.Trim() + if ($line -match '^\s*#' -or $line -match '^\s*$') { return } + if ($line -match '^\s*([^=]+)=(.+)$') { + $key = $matches[1].Trim() + $val = $matches[2].Trim('"'' ') + $val = $val -replace '\$HOME', [Regex]::Escape($HOME) + $val = $val -replace '^~', [Regex]::Escape($HOME) + $val = $val -replace '\\\\','\' + $Rc[$key] = $val + } + } +} +function RcOr([string]$Key,[string]$Fallback){ if($Rc.ContainsKey($Key) -and $Rc[$Key]){$Rc[$Key]}else{$Fallback} } + +if (-not $AdminUser) { $AdminUser = RcOr "DEFAULT_ADMIN_USER" "vdidev" } +if (-not $AdminEmail) { $AdminEmail = RcOr "DEFAULT_ADMIN_EMAIL" "dev@vincentdesign.ca" } +if (-not $ThemeStarterRepo) { $ThemeStarterRepo = RcOr "THEME_STARTER_REPO" "https://github.com/WordPress/twentytwentyfour" } +if (-not $HerdWorkspace) { $HerdWorkspace = RcOr "HERD_WORKSPACE" "$HOME\Herd" } +if (-not $LocalTld) { $LocalTld = RcOr "LOCAL_TLD" "test" } +if (-not $MysqlHost) { $MysqlHost = RcOr "MYSQL_HOST" "127.0.0.1" } +if (-not $MysqlPort) { $MysqlPort = RcOr "MYSQL_PORT" "3306" } +if (-not $MysqlRootUser) { $MysqlRootUser = RcOr "MYSQL_ROOT_USER" "root" } +if (-not $MysqlRootPass) { $MysqlRootPass = RcOr "MYSQL_ROOT_PASS" "" } + +# ---- Helpers ---- +function Slugify([string]$s){ ($s.ToLower() -replace '[^a-z0-9]+','-').Trim('-') } + +function RandPass(){ + $b = New-Object 'System.Byte[]' 48 + (New-Object System.Security.Cryptography.RNGCryptoServiceProvider).GetBytes($b) + $raw = [Convert]::ToBase64String($b) -replace '[^a-zA-Z0-9]','' + while($raw.Length -lt 24){ + $b2 = New-Object 'System.Byte[]' 16 + (New-Object System.Security.Cryptography.RNGCryptoServiceProvider).GetBytes($b2) + $raw += ([Convert]::ToBase64String($b2) -replace '[^a-zA-Z0-9]','') + } + $raw.Substring(0,24) } +# Resolve script folder + absolute phar +$ScriptRoot = if ($PSScriptRoot) { $PSScriptRoot } else { Split-Path -Parent $MyInvocation.MyCommand.Definition } + +$PharFullPath= Join-Path $ScriptRoot 'wp-cli.phar' + +if (!(Test-Path $PharFullPath)) { throw "wp-cli.phar not found at: $PharFullPath" } + +# Resolve Herd php.exe (parse php.bat or derive from php -v → $HOME\.config\herd\bin\\php.exe) +function Resolve-PhpExe { + $phpCmd = Get-Command php -ErrorAction SilentlyContinue + if ($phpCmd -and ($phpCmd.Source -match '\.bat$') -and (Test-Path $phpCmd.Source)) { + $bat = Get-Content -Raw $phpCmd.Source + $m = [regex]::Match($bat, '(["'']?)(%[^"''\r\n]+%|[A-Za-z]:\\[^"''\r\n]+?\\php\.exe)\1') + if ($m.Success) { + $raw = $m.Groups[2].Value + $exp = [Environment]::ExpandEnvironmentVariables($raw) + if (Test-Path $exp) { return $exp } + } + } + + try { + $verOut = & cmd.exe /d /s /c 'php -v' 2>&1 + $verLine = ($verOut | Select-Object -First 1) + $m2 = [regex]::Match($verLine, 'PHP\s+(\d+\.\d+\.\d+)') + if ($m2.Success) { + $ver = $m2.Groups[1].Value + $cand = Join-Path $HOME ".config\herd\bin\$ver\php.exe" + if (Test-Path $cand) { return $cand } + } + } catch {} + $whereExe = (& where.exe php 2>$null) -split "`r?`n" | Where-Object { $_ -match '\.exe$' } | Select-Object -First 1 + if ($whereExe -and (Test-Path $whereExe)) { return $whereExe } + throw "Could not resolve a real php.exe (expected like $HOME\.config\herd\bin\\php.exe)." +} + +$PhpExe = Resolve-PhpExe + +# ---- WP-CLI wrapper: space-safe (runs php from script folder) ---- function Invoke-WP { - param([string[]]$Args) - # Support 'wp' OR 'php .\wp-cli.phar' - if ($WpCliPath -match '\s') { - $parts = $WpCliPath -split '\s+' - & $parts[0] $parts[1..($parts.Count-1)] $Args 2>&1 | ForEach-Object { "$_" } - } else { - & $WpCliPath $Args 2>&1 | ForEach-Object { "$_" } + [CmdletBinding(PositionalBinding=$false)] + param( + [Parameter(ValueFromRemainingArguments=$true)] + [string[]]$WpArgs + ) + + # WordPress installation directory (do not change) + $wpPath = ((Get-Location).Path) -replace '\\','/' + + # Always run PHP from the folder that contains wp-cli.phar to avoid spaces in the script path + $orig = Get-Location + try { + Push-Location $ScriptRoot + + # Use -f and -- to separate PHP options from WP-CLI args + # Format executed: php -f wp-cli.phar -- --path= + $all = @("-f", "wp-cli.phar", "--", "--path=$wpPath") + $WpArgs + + # DEBUG (optional): uncomment to see exactly what's executed + # Write-Host ">> $PhpExe $($all -join ' ') (cwd=$((Get-Location).Path))" + + $out = & $PhpExe @all 2>&1 + + # DEBUG (optional): uncomment to see full output + # $out | Out-Host + + if ($LASTEXITCODE -ne 0) { + throw "WP-CLI failed (exit $LASTEXITCODE): $PhpExe $($all -join ' ') (cwd=$((Get-Location).Path))" + } + return $out + } + finally { + Pop-Location | Out-Null } - if ($LASTEXITCODE -ne 0) { throw "WP-CLI command failed: $($Args -join ' ')" } } -function WP([string]$line) { - # Convenience to pass a single-line string; splits on spaces - $args = @() - $token = "" - $inQuote = $false - foreach ($c in $line.ToCharArray()) { - if ($c -eq '"') { $inQuote = -not $inQuote; continue } - if (-not $inQuote -and [char]::IsWhiteSpace($c)) { - if ($token.Length -gt 0) { $args += $token; $token = "" } - } else { $token += $c } - } - if ($token.Length -gt 0) { $args += $token } - Invoke-WP $args -} - -# === Start === -$Slug = Slugify $ProjectName -$ProjectDir = Join-Path $HerdWorkspace $Slug -$LocalUrl = "http://$Slug.$LocalTld" - -$DbName = "vdi_$Slug" -$DbUser = $DbName -$DbPass = RandPass -$AdminPass = RandPass +# ---- Compute basics ---- +$Slug = Slugify $ProjectName +$ProjectDir = Join-Path $HerdWorkspace $Slug +$LocalUrl = "http://$Slug.$LocalTld" +$DbName = "vdi_$Slug" +$DbUser = $DbName +$DbPass = RandPass +$AdminPass = RandPass Write-Host "Creating project at $ProjectDir..." New-Item -ItemType Directory -Force -Path $ProjectDir | Out-Null Set-Location $ProjectDir -# --- WordPress Core --- -Write-Host "Downloading WordPress..." -Invoke-WP @("core","download","--force") +# Sanity print +$pathShown = ((Get-Location).Path -replace '\\','/') +Write-Host "WP-CLI php: $PhpExe" +Write-Host "WP-CLI phar: $PharFullPath" +Write-Host "WP-CLI --path: --path=$pathShown" -# --- Database setup --- +# ---- WP Core ---- +Invoke-WP "cli" "version" +# Invoke-WP "--info" +Invoke-WP "core" "download" "--force" + +# ---------- Database (DBngin/MySQL) ---------- Write-Host "Creating MySQL DB and user..." -$mysqlArgs = @("-h", $MysqlHost, "-P", $MysqlPort, "-u", $MysqlRootUser) -if ($MysqlRootPass -ne "") { $mysqlArgs += "-p$MysqlRootPass" } -$sql = @" -CREATE DATABASE IF NOT EXISTS `$DbName` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -CREATE USER IF NOT EXISTS '$DbUser'@'%' IDENTIFIED BY '$DbPass'; -GRANT ALL PRIVILEGES ON `$DbName`.* TO '$DbUser'@'%'; -FLUSH PRIVILEGES; -"@ -try { - $sql | & mysql.exe @mysqlArgs -} catch { - throw "MySQL client not found or connection failed. Ensure mysql.exe is in PATH and DBngin/MySQL is running." + +# Build root arg list +$mysqlArgsRoot = @("-h", $MysqlHost, "-P", $MysqlPort, "-u", $MysqlRootUser) +if ($MysqlRootPass -ne "") { $mysqlArgsRoot += "-p$MysqlRootPass" } + +# Sanity: confirm root can connect +$null = & mysql.exe @mysqlArgsRoot -e "SELECT VERSION();" 2>$null +if ($LASTEXITCODE -ne 0) { + throw ("Cannot connect to MySQL as root ({0}@{1}:{2}). Check MYSQL_ROOT_* in your rc file." -f $MysqlRootUser, $MysqlHost, $MysqlPort) } -# --- Config + Install --- -Invoke-WP @("config","create","--dbname=$DbName","--dbuser=$DbUser","--dbpass=$DbPass","--dbhost=$MysqlHost`:$MysqlPort","--force") -Invoke-WP @("config","shuffle-salts") -Invoke-WP @("core","install","--url=$LocalUrl","--title=$ProjectName","--admin_user=$AdminUser","--admin_password=$AdminPass","--admin_email=$AdminEmail") -Invoke-WP @("option","update","siteurl",$LocalUrl) -Invoke-WP @("option","update","home",$LocalUrl) +# Build SQL without any escaping sequences in PowerShell strings. +# Use a real backtick char for MySQL identifiers: +$bt = [char]96 # ` +$dbIdent = "$bt$DbName$bt" -# --- Pages & Reading --- +$sqlParts = @( + "CREATE DATABASE IF NOT EXISTS $dbIdent CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;", + "", + "CREATE USER IF NOT EXISTS '$DbUser'@'localhost' IDENTIFIED BY '$DbPass';", + "CREATE USER IF NOT EXISTS '$DbUser'@'127.0.0.1' IDENTIFIED BY '$DbPass';", + "CREATE USER IF NOT EXISTS '$DbUser'@'%' IDENTIFIED BY '$DbPass';", + "", + "GRANT ALL PRIVILEGES ON $dbIdent.* TO '$DbUser'@'localhost';", + "GRANT ALL PRIVILEGES ON $dbIdent.* TO '$DbUser'@'127.0.0.1';", + "GRANT ALL PRIVILEGES ON $dbIdent.* TO '$DbUser'@'%';", + "FLUSH PRIVILEGES;" +) + +$tmpSql = [System.IO.Path]::GetTempFileName() +[System.IO.File]::WriteAllLines($tmpSql, $sqlParts, [System.Text.Encoding]::UTF8) + +# Feed the SQL file to mysql via cmd redirection (reliable on Windows) +function Quote-ForCmd([string]$s){ '"' + ($s -replace '"','""') + '"' } + +$cmdLine = "mysql.exe " + ( + ($mysqlArgsRoot | ForEach-Object { Quote-ForCmd $_ }) -join ' ' +) + " < " + (Quote-ForCmd $tmpSql) + +# DEBUG (optional): uncomment to see exactly what's run +# Write-Host ">> $cmdLine" + +& cmd.exe /d /s /c $cmdLine +$exit = $LASTEXITCODE + +# Clean up the temp file regardless +Remove-Item -Force $tmpSql -ErrorAction SilentlyContinue + +if ($exit -ne 0) { + throw "MySQL SQL error while creating DB/user. Verify root creds and server." +} + +# Verify the NEW user can actually select the DB (fail fast if not) +$mysqlArgsUser = @("-h", $MysqlHost, "-P", $MysqlPort, "-u", $DbUser, "-p$DbPass", "-D", $DbName) +$null = & mysql.exe @mysqlArgsUser -e "SELECT 1;" 2>$null +if ($LASTEXITCODE -ne 0) { + throw ("New MySQL user cannot access DB '{0}' via {1}:{2}. Host match or privileges issue — check users and grants." -f $DbName, $MysqlHost, $MysqlPort) +} + +# ---- Config + Install ---- +Invoke-WP "config" "create" "--dbname=$DbName" "--dbuser=$DbUser" "--dbpass=$DbPass" "--dbhost=$($MysqlHost):$($MysqlPort)" "--force" +Invoke-WP "config" "shuffle-salts" +Invoke-WP "core" "install" "--url=$LocalUrl" "--title=$ProjectName" "--admin_user=$AdminUser" "--admin_password=$AdminPass" "--admin_email=$AdminEmail" +Invoke-WP "option" "update" "siteurl" $LocalUrl +Invoke-WP "option" "update" "home" $LocalUrl + +# ---- Pages & Reading ---- Write-Host "Creating Home/News pages and setting permalinks..." -$homeId = (Invoke-WP @("post","create","--post_type=page","--post_status=publish","--post_title=Home","--porcelain") | Select-Object -Last 1).Trim() -$newsId = (Invoke-WP @("post","create","--post_type=page","--post_status=publish","--post_title=News","--porcelain") | Select-Object -Last 1).Trim() -Invoke-WP @("option","update","show_on_front","page") -Invoke-WP @("option","update","page_on_front",$homeId) -Invoke-WP @("option","update","page_for_posts",$newsId) -Invoke-WP @("rewrite","structure","/%postname%/") -Invoke-WP @("rewrite","flush","--hard") +$homeId = (Invoke-WP "post" "create" "--post_type=page" "--post_status=publish" "--post_title=Home" "--porcelain" | Select-Object -Last 1).Trim() +$newsId = (Invoke-WP "post" "create" "--post_type=page" "--post_status=publish" "--post_title=News" "--porcelain" | Select-Object -Last 1).Trim() +Invoke-WP "option" "update" "show_on_front" "page" +Invoke-WP "option" "update" "page_on_front" $homeId +Invoke-WP "option" "update" "page_for_posts" $newsId +Invoke-WP "rewrite" "structure" "/%postname%/" +Invoke-WP "rewrite" "flush" "--hard" -# --- Theme --- +# --- Ensure .htaccess exists (for Apache/permalinks) --- +$htFile = Join-Path $ProjectDir ".htaccess" +if (!(Test-Path $htFile)) { + $ht = @" +# BEGIN WordPress + +RewriteEngine On +RewriteBase / +RewriteRule ^index\.php$ - [L] +RewriteCond %{REQUEST_FILENAME} !-f +RewriteCond %{REQUEST_FILENAME} !-d +RewriteRule . /index.php [L] + +# END WordPress +"@ + Set-Content -Path $htFile -Value $ht -Encoding ASCII + Write-Host "Created default .htaccess" +} + +# ---- Theme ---- Write-Host "Cloning starter theme..." $tmp = ".starter-tmp" if (Test-Path $tmp) { Remove-Item -Recurse -Force $tmp } git clone --depth=1 $ThemeStarterRepo $tmp | Out-Null -$ThemeDir = "wp-content\themes\$Slug-theme" +$ThemeDir = "wp-content\themes\$Slug" if (Test-Path $ThemeDir) { Remove-Item -Recurse -Force $ThemeDir } New-Item -ItemType Directory -Force -Path (Split-Path $ThemeDir) | Out-Null Move-Item $tmp $ThemeDir if (Test-Path "$ThemeDir\.git") { Remove-Item -Recurse -Force "$ThemeDir\.git" } -Invoke-WP @("theme","activate","$Slug-theme") +Invoke-WP "theme" "activate" "$Slug" Push-Location $ThemeDir +# ---- Init theme dependencies ---- +composer install 2>$null +npm install 2>$null + +# ---- Init new git repo ---- git init -b main | Out-Null git add -A git commit -m "feat: bootstrap ${ProjectName} theme from starter" | Out-Null Pop-Location -# --- Plugins --- -if (Test-Path "plugins.json") { +# ---------- Plugins (optional) ---------- +# Look for plugins.json in the project, then fall back to script folder +$PluginsFileProject = Join-Path $ProjectDir "plugins.json" +$PluginsFileScript = Join-Path $ScriptRoot "plugins.json" +$PluginsFile = if (Test-Path $PluginsFileProject) { $PluginsFileProject } + elseif (Test-Path $PluginsFileScript) { $PluginsFileScript } + else { $null } + +if ($PluginsFile) { try { - $plugins = Get-Content "plugins.json" | ConvertFrom-Json + Write-Host "Installing plugins from $PluginsFile..." + $plugins = Get-Content $PluginsFile -Raw | ConvertFrom-Json foreach ($plugin in $plugins) { if ($plugin.zip) { $args = @("plugin","install",$plugin.zip,"--force") if ($plugin.activate -eq $true) { $args += "--activate" } - Invoke-WP $args + Invoke-WP @args } elseif ($plugin.slug) { $args = @("plugin","install",$plugin.slug,"--force") if ($plugin.version) { $args += "--version=$($plugin.version)" } if ($plugin.activate -eq $true) { $args += "--activate" } - Invoke-WP $args + Invoke-WP @args } } } catch { - Write-Host "Error reading plugins.json: $_" + Write-Host "Plugin install error: $_" } +} else { + Write-Host "No plugins.json found in project or script folder; skipping plugins." } -# --- Summary --- +# ---- Summary ---- $Summary = @" VDI WP Bootstrap — Summary =========================== diff --git a/windows/wp-cli.phar b/windows/wp-cli.phar new file mode 100644 index 0000000..4432e71 Binary files /dev/null and b/windows/wp-cli.phar differ