From 28407dafbce619f559871971128940f162f03559 Mon Sep 17 00:00:00 2001 From: Wojtek Naruniec Date: Tue, 17 Mar 2026 16:44:01 +0100 Subject: [PATCH 01/13] Add phpMyAdmin data model and CLI server support - Add enablePhpMyAdmin field to siteDetailsSchema and ipc-types.d.ts - Add DEFAULT_ENABLE_PHPMYADMIN = false constant for easy toggling - Add enablePhpMyAdmin to ServerConfig IPC type - Pass enablePhpMyAdmin from site data through to RunCLI args (phpmyadmin: true) - Add phpmyadminChanged to SiteSettingChanges and siteNeedsRestart --- apps/cli/lib/types/wordpress-server-ipc.ts | 1 + apps/cli/lib/wordpress-server-manager.ts | 4 ++++ apps/cli/wordpress-server-child.ts | 5 +++++ tools/common/constants.ts | 3 +++ tools/common/lib/site-events.ts | 1 + tools/common/lib/site-needs-restart.ts | 5 ++++- 6 files changed, 18 insertions(+), 1 deletion(-) diff --git a/apps/cli/lib/types/wordpress-server-ipc.ts b/apps/cli/lib/types/wordpress-server-ipc.ts index 0108b5890f..e6a60c7b95 100644 --- a/apps/cli/lib/types/wordpress-server-ipc.ts +++ b/apps/cli/lib/types/wordpress-server-ipc.ts @@ -17,6 +17,7 @@ const serverConfig = z.object( { enableXdebug: z.boolean().optional(), enableDebugLog: z.boolean().optional(), enableDebugDisplay: z.boolean().optional(), + enablePhpMyAdmin: z.boolean().optional(), blueprint: z .object( { contents: z.any(), // Blueprint type is complex, allow any for now diff --git a/apps/cli/lib/wordpress-server-manager.ts b/apps/cli/lib/wordpress-server-manager.ts index e74249d887..5ce92d72e7 100644 --- a/apps/cli/lib/wordpress-server-manager.ts +++ b/apps/cli/lib/wordpress-server-manager.ts @@ -115,6 +115,10 @@ export async function startWordPressServer( serverConfig.enableDebugDisplay = true; } + if ( site.enablePhpMyAdmin ) { + serverConfig.enablePhpMyAdmin = true; + } + const processDesc = await startProcess( processName, wordPressServerChildPath ); await waitForReadyMessage( processDesc.pmId ); await sendMessage( diff --git a/apps/cli/wordpress-server-child.ts b/apps/cli/wordpress-server-child.ts index fda5a5821c..49fc70af22 100644 --- a/apps/cli/wordpress-server-child.ts +++ b/apps/cli/wordpress-server-child.ts @@ -260,6 +260,11 @@ async function getBaseRunCLIArgs( args.xdebug = true; } + if ( config.enablePhpMyAdmin ) { + logToConsole( 'Enabling phpMyAdmin support' ); + args.phpmyadmin = true; + } + return args; } diff --git a/tools/common/constants.ts b/tools/common/constants.ts index 515e708740..fa3693fc1f 100644 --- a/tools/common/constants.ts +++ b/tools/common/constants.ts @@ -32,3 +32,6 @@ export const MINIMUM_WORDPRESS_VERSION = '6.2.1' as const; // https://wordpress. export const DEFAULT_WORDPRESS_VERSION = 'latest' as const; export const DEFAULT_PHP_VERSION: typeof RecommendedPHPVersion = RecommendedPHPVersion; export const SQLITE_FILENAME = 'sqlite-database-integration' as const; + +// phpMyAdmin - set to true to enable by default for all new sites +export const DEFAULT_ENABLE_PHPMYADMIN = false; diff --git a/tools/common/lib/site-events.ts b/tools/common/lib/site-events.ts index ec8f25ca02..390d484bd6 100644 --- a/tools/common/lib/site-events.ts +++ b/tools/common/lib/site-events.ts @@ -26,6 +26,7 @@ export const siteDetailsSchema = z.object( { enableXdebug: z.boolean().optional(), enableDebugLog: z.boolean().optional(), enableDebugDisplay: z.boolean().optional(), + enablePhpMyAdmin: z.boolean().optional(), } ); export type SiteDetails = z.infer< typeof siteDetailsSchema >; diff --git a/tools/common/lib/site-needs-restart.ts b/tools/common/lib/site-needs-restart.ts index 870ae81a7d..99219b54cc 100644 --- a/tools/common/lib/site-needs-restart.ts +++ b/tools/common/lib/site-needs-restart.ts @@ -7,6 +7,7 @@ export interface SiteSettingChanges { credentialsChanged?: boolean; debugLogChanged?: boolean; debugDisplayChanged?: boolean; + phpmyadminChanged?: boolean; } export function siteNeedsRestart( changes: SiteSettingChanges ): boolean { @@ -19,6 +20,7 @@ export function siteNeedsRestart( changes: SiteSettingChanges ): boolean { credentialsChanged, debugLogChanged, debugDisplayChanged, + phpmyadminChanged, } = changes; return !! ( @@ -29,6 +31,7 @@ export function siteNeedsRestart( changes: SiteSettingChanges ): boolean { xdebugChanged || credentialsChanged || debugLogChanged || - debugDisplayChanged + debugDisplayChanged || + phpmyadminChanged ); } From 5464e716c15aba166eb4e033e292edde8f659f6a Mon Sep 17 00:00:00 2001 From: Wojtek Naruniec Date: Tue, 17 Mar 2026 16:44:10 +0100 Subject: [PATCH 02/13] Add phpMyAdmin support to site set command and desktop app bridge - Add --phpmyadmin / --no-phpmyadmin to CLI site set command - Persist enablePhpMyAdmin in appdata via site set - Add phpmyadmin to EditSiteOptions and buildCliArgs - Detect enablePhpMyAdmin changes in updateSite IPC handler --- apps/cli/commands/site/set.ts | 20 ++++++++++++++++--- apps/studio/src/ipc-handlers.ts | 4 ++++ apps/studio/src/ipc-types.d.ts | 1 + .../src/modules/cli/lib/cli-site-editor.ts | 5 +++++ 4 files changed, 27 insertions(+), 3 deletions(-) diff --git a/apps/cli/commands/site/set.ts b/apps/cli/commands/site/set.ts index e4894151e4..e98ece0add 100644 --- a/apps/cli/commands/site/set.ts +++ b/apps/cli/commands/site/set.ts @@ -54,6 +54,7 @@ export interface SetCommandOptions { adminEmail?: string; debugLog?: boolean; debugDisplay?: boolean; + phpmyadmin?: boolean; } export async function runCommand( sitePath: string, options: SetCommandOptions ): Promise< void > { @@ -68,6 +69,7 @@ export async function runCommand( sitePath: string, options: SetCommandOptions ) adminPassword, debugLog, debugDisplay, + phpmyadmin, } = options; let { adminEmail } = options; @@ -82,11 +84,12 @@ export async function runCommand( sitePath: string, options: SetCommandOptions ) adminPassword === undefined && adminEmail === undefined && debugLog === undefined && - debugDisplay === undefined + debugDisplay === undefined && + phpmyadmin === undefined ) { throw new LoggerError( __( - 'At least one option (--name, --domain, --https, --php, --wp, --xdebug, --admin-username, --admin-password, --admin-email, --debug-log, --debug-display) is required.' + 'At least one option (--name, --domain, --https, --php, --wp, --xdebug, --admin-username, --admin-password, --admin-email, --debug-log, --debug-display, --phpmyadmin) is required.' ) ); } @@ -171,6 +174,7 @@ export async function runCommand( sitePath: string, options: SetCommandOptions ) const debugLogChanged = debugLog !== undefined && debugLog !== site.enableDebugLog; const debugDisplayChanged = debugDisplay !== undefined && debugDisplay !== site.enableDebugDisplay; + const phpmyadminChanged = phpmyadmin !== undefined && phpmyadmin !== site.enablePhpMyAdmin; const hasChanges = nameChanged || @@ -181,7 +185,8 @@ export async function runCommand( sitePath: string, options: SetCommandOptions ) xdebugChanged || credentialsChanged || debugLogChanged || - debugDisplayChanged; + debugDisplayChanged || + phpmyadminChanged; if ( ! hasChanges ) { throw new LoggerError( __( 'No changes to apply. The site already has the specified settings.' ) @@ -197,6 +202,7 @@ export async function runCommand( sitePath: string, options: SetCommandOptions ) credentialsChanged, debugLogChanged, debugDisplayChanged, + phpmyadminChanged, } ); const oldDomain = site.customDomain; @@ -238,6 +244,9 @@ export async function runCommand( sitePath: string, options: SetCommandOptions ) if ( debugDisplayChanged ) { foundSite.enableDebugDisplay = debugDisplay; } + if ( phpmyadminChanged ) { + foundSite.enablePhpMyAdmin = phpmyadmin; + } await saveAppdata( appdata ); site = foundSite; @@ -392,6 +401,10 @@ export const registerCommand = ( yargs: StudioArgv ) => { .option( 'debug-display', { type: 'boolean', description: __( 'Enable WP_DEBUG_DISPLAY' ), + } ) + .option( 'phpmyadmin', { + type: 'boolean', + description: __( 'Enable phpMyAdmin' ), } ); }, handler: async ( argv ) => { @@ -408,6 +421,7 @@ export const registerCommand = ( yargs: StudioArgv ) => { adminEmail: argv.adminEmail, debugLog: argv.debugLog, debugDisplay: argv.debugDisplay, + phpmyadmin: argv.phpmyadmin, } ); } catch ( error ) { if ( error instanceof LoggerError ) { diff --git a/apps/studio/src/ipc-handlers.ts b/apps/studio/src/ipc-handlers.ts index 1b34ed5e9b..8d409f3014 100644 --- a/apps/studio/src/ipc-handlers.ts +++ b/apps/studio/src/ipc-handlers.ts @@ -491,6 +491,10 @@ export async function updateSite( options.debugDisplay = updatedSite.enableDebugDisplay ?? false; } + if ( updatedSite.enablePhpMyAdmin !== currentSite.enablePhpMyAdmin ) { + options.phpmyadmin = updatedSite.enablePhpMyAdmin ?? false; + } + const hasCliChanges = Object.keys( options ).length > 2; if ( hasCliChanges ) { diff --git a/apps/studio/src/ipc-types.d.ts b/apps/studio/src/ipc-types.d.ts index e07e54d5e5..129bf45d64 100644 --- a/apps/studio/src/ipc-types.d.ts +++ b/apps/studio/src/ipc-types.d.ts @@ -35,6 +35,7 @@ interface StoppedSiteDetails { enableXdebug?: boolean; enableDebugLog?: boolean; enableDebugDisplay?: boolean; + enablePhpMyAdmin?: boolean; sortOrder?: number; } diff --git a/apps/studio/src/modules/cli/lib/cli-site-editor.ts b/apps/studio/src/modules/cli/lib/cli-site-editor.ts index 1b978ed654..b02ef9e15a 100644 --- a/apps/studio/src/modules/cli/lib/cli-site-editor.ts +++ b/apps/studio/src/modules/cli/lib/cli-site-editor.ts @@ -22,6 +22,7 @@ export interface EditSiteOptions { adminEmail?: string; debugLog?: boolean; debugDisplay?: boolean; + phpmyadmin?: boolean; } export async function editSiteViaCli( options: EditSiteOptions ): Promise< void > { @@ -104,5 +105,9 @@ function buildCliArgs( options: EditSiteOptions ): string[] { args.push( options.debugDisplay ? '--debug-display' : '--no-debug-display' ); } + if ( options.phpmyadmin !== undefined ) { + args.push( options.phpmyadmin ? '--phpmyadmin' : '--no-phpmyadmin' ); + } + return args; } From 82a027aeccff26081ab7d324e16d46a2c6a93302 Mon Sep 17 00:00:00 2001 From: Wojtek Naruniec Date: Tue, 17 Mar 2026 16:44:18 +0100 Subject: [PATCH 03/13] Add phpMyAdmin toggle in Debugging tab and button on Overview tab MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add Enable phpMyAdmin checkbox in Edit Site → Debugging tab - Checkbox is disabled with offline tooltip when user has no internet - Add phpMyAdmin button to Open in… section on Overview tab - Button only shown when phpMyAdmin is enabled for the site - Button is disabled with offline tooltip when user has no internet --- .../src/components/content-tab-overview.tsx | 43 +++++++++++++- .../site-settings/edit-site-details.tsx | 57 ++++++++++++++++++- 2 files changed, 97 insertions(+), 3 deletions(-) diff --git a/apps/studio/src/components/content-tab-overview.tsx b/apps/studio/src/components/content-tab-overview.tsx index 6055a8931b..2060c24cd9 100644 --- a/apps/studio/src/components/content-tab-overview.tsx +++ b/apps/studio/src/components/content-tab-overview.tsx @@ -16,7 +16,11 @@ import { import { useI18n } from '@wordpress/react-i18n'; import { useState } from 'react'; import { ArrowIcon } from 'src/components/arrow-icon'; +import Button from 'src/components/button'; import { ButtonsSection, ButtonsSectionProps } from 'src/components/buttons-section'; +import offlineIcon from 'src/components/offline-icon'; +import { Tooltip } from 'src/components/tooltip'; +import { useOffline } from 'src/hooks/use-offline'; import { useSiteDetails } from 'src/hooks/use-site-details'; import { useThemeDetails } from 'src/hooks/use-theme-details'; import { isWindows } from 'src/lib/app-globals'; @@ -134,6 +138,9 @@ function CustomizeSection( { function ShortcutsSection( { selectedSite }: Pick< ContentTabOverviewProps, 'selectedSite' > ) { const { data: editor } = useGetUserEditorQuery(); const { data: terminal } = useGetUserTerminalQuery(); + const { startServer, loadingServer } = useSiteDetails(); + const isOffline = useOffline(); + const isServerLoading = loadingServer[ selectedSite.id ]; const buttonsArray: ButtonsSectionProps[ 'buttonsArray' ] = [ { @@ -176,7 +183,41 @@ function ShortcutsSection( { selectedSite }: Pick< ContentTabOverviewProps, 'sel } }, } ); - return ; + const phpMyAdminButton = selectedSite.enablePhpMyAdmin ? ( + +
+ +
+
+ ) : null; + + return ( +
+ + { phpMyAdminButton &&
{ phpMyAdminButton }
} +
+ ); } export function ContentTabOverview( { selectedSite }: ContentTabOverviewProps ) { diff --git a/apps/studio/src/modules/site-settings/edit-site-details.tsx b/apps/studio/src/modules/site-settings/edit-site-details.tsx index e65e2b403c..6286f8b1c5 100644 --- a/apps/studio/src/modules/site-settings/edit-site-details.tsx +++ b/apps/studio/src/modules/site-settings/edit-site-details.tsx @@ -1,4 +1,8 @@ -import { DEFAULT_PHP_VERSION, DEFAULT_WORDPRESS_VERSION } from '@studio/common/constants'; +import { + DEFAULT_ENABLE_PHPMYADMIN, + DEFAULT_PHP_VERSION, + DEFAULT_WORDPRESS_VERSION, +} from '@studio/common/constants'; import { generateCustomDomainFromSiteName, getDomainNameValidationError, @@ -20,10 +24,12 @@ import Button from 'src/components/button'; import { ErrorInformation } from 'src/components/error-information'; import { LearnMoreLink, LearnHowLink } from 'src/components/learn-more'; import Modal from 'src/components/modal'; +import offlineIcon from 'src/components/offline-icon'; import PasswordControl from 'src/components/password-control'; import TextControlComponent from 'src/components/text-control'; import { Tooltip } from 'src/components/tooltip'; import { WPVersionSelector } from 'src/components/wp-version-selector'; +import { useOffline } from 'src/hooks/use-offline'; import { useSiteDetails } from 'src/hooks/use-site-details'; import { cx } from 'src/lib/cx'; import { getIpcApi } from 'src/lib/get-ipc-api'; @@ -37,6 +43,7 @@ type EditSiteDetailsProps = { const EditSiteDetails = ( { currentWpVersion, onSave }: EditSiteDetailsProps ) => { const { __ } = useI18n(); const { updateSite, selectedSite, isEditModalOpen, setIsEditModalOpen } = useSiteDetails(); + const isOffline = useOffline(); const [ errorUpdatingWpVersion, setErrorUpdatingWpVersion ] = useState< string | null >( null ); const [ isEditingSite, setIsEditingSite ] = useState( false ); const [ needsRestart, setNeedsRestart ] = useState( false ); @@ -45,6 +52,9 @@ const EditSiteDetails = ( { currentWpVersion, onSave }: EditSiteDetailsProps ) = const [ enableDebugDisplay, setEnableDebugDisplay ] = useState( selectedSite?.enableDebugDisplay ?? false ); + const [ enablePhpMyAdmin, setEnablePhpMyAdmin ] = useState( + selectedSite?.enablePhpMyAdmin ?? DEFAULT_ENABLE_PHPMYADMIN + ); const [ xdebugEnabledSite, setXdebugEnabledSite ] = useState< SiteDetails | null >( null ); const [ adminUsername, setAdminUsername ] = useState( selectedSite?.adminUsername ?? 'admin' ); const [ adminPassword, setAdminPassword ] = useState( @@ -146,7 +156,8 @@ const EditSiteDetails = ( { currentWpVersion, onSave }: EditSiteDetailsProps ) = ( decodePassword( selectedSite.adminPassword ?? '' ) || 'password' ) === adminPassword && ( selectedSite.adminEmail || 'admin@localhost.com' ) === adminEmail && !! selectedSite.enableDebugLog === enableDebugLog && - !! selectedSite.enableDebugDisplay === enableDebugDisplay; + !! selectedSite.enableDebugDisplay === enableDebugDisplay && + ( selectedSite.enablePhpMyAdmin ?? DEFAULT_ENABLE_PHPMYADMIN ) === enablePhpMyAdmin; const hasValidationErrors = ! selectedSite || ! siteName.trim() || @@ -173,6 +184,7 @@ const EditSiteDetails = ( { currentWpVersion, onSave }: EditSiteDetailsProps ) = setAdminEmail( selectedSite.adminEmail || 'admin@localhost.com' ); setEnableDebugLog( selectedSite.enableDebugLog ?? false ); setEnableDebugDisplay( selectedSite.enableDebugDisplay ?? false ); + setEnablePhpMyAdmin( selectedSite.enablePhpMyAdmin ?? DEFAULT_ENABLE_PHPMYADMIN ); }, [ selectedSite, getEffectiveWpVersion ] ); const onSiteEdit = async ( event: FormEvent ) => { @@ -189,6 +201,8 @@ const EditSiteDetails = ( { currentWpVersion, onSave }: EditSiteDetailsProps ) = const hasDebugLogChanged = enableDebugLog !== ( selectedSite.enableDebugLog ?? false ); const hasDebugDisplayChanged = enableDebugDisplay !== ( selectedSite.enableDebugDisplay ?? false ); + const hasPhpMyAdminChanged = + enablePhpMyAdmin !== ( selectedSite.enablePhpMyAdmin ?? DEFAULT_ENABLE_PHPMYADMIN ); const hasDomainChanged = Boolean( selectedSite.customDomain ) !== useCustomDomain || ( useCustomDomain && customDomain !== selectedSite.customDomain ); @@ -210,6 +224,7 @@ const EditSiteDetails = ( { currentWpVersion, onSave }: EditSiteDetailsProps ) = credentialsChanged: hasCredentialsChanged, debugLogChanged: hasDebugLogChanged, debugDisplayChanged: hasDebugDisplayChanged, + phpmyadminChanged: hasPhpMyAdminChanged, } ); setNeedsRestart( needsRestart ); @@ -235,6 +250,7 @@ const EditSiteDetails = ( { currentWpVersion, onSave }: EditSiteDetailsProps ) = adminEmail, enableDebugLog, enableDebugDisplay, + enablePhpMyAdmin, }, hasWpVersionChanged ? selectedWpVersion : undefined ); @@ -589,6 +605,43 @@ const EditSiteDetails = ( { currentWpVersion, onSave }: EditSiteDetailsProps ) = ) } + +
+ +
+ setEnablePhpMyAdmin( e.target.checked ) } + disabled={ isEditingSite || isOffline } + /> + +
+
+ { __( + 'Access phpMyAdmin to browse and manage your site database. Requires internet to download.' + ) } +
+
+
) } From b3c5a0139e731645fa69e7fedd238260dae55471 Mon Sep 17 00:00:00 2001 From: Wojtek Naruniec Date: Tue, 17 Mar 2026 16:49:29 +0100 Subject: [PATCH 04/13] =?UTF-8?q?Move=20phpMyAdmin=20button=20into=20Open?= =?UTF-8?q?=20in=E2=80=A6=20grid;=20always=20show,=20disable=20when=20off?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/content-tab-overview.tsx | 55 ++++++------------- 1 file changed, 18 insertions(+), 37 deletions(-) diff --git a/apps/studio/src/components/content-tab-overview.tsx b/apps/studio/src/components/content-tab-overview.tsx index 2060c24cd9..6bda7d61b7 100644 --- a/apps/studio/src/components/content-tab-overview.tsx +++ b/apps/studio/src/components/content-tab-overview.tsx @@ -4,6 +4,7 @@ import { archive, code, desktop, + grid, pencil, layout, navigation, @@ -16,10 +17,7 @@ import { import { useI18n } from '@wordpress/react-i18n'; import { useState } from 'react'; import { ArrowIcon } from 'src/components/arrow-icon'; -import Button from 'src/components/button'; import { ButtonsSection, ButtonsSectionProps } from 'src/components/buttons-section'; -import offlineIcon from 'src/components/offline-icon'; -import { Tooltip } from 'src/components/tooltip'; import { useOffline } from 'src/hooks/use-offline'; import { useSiteDetails } from 'src/hooks/use-site-details'; import { useThemeDetails } from 'src/hooks/use-theme-details'; @@ -183,41 +181,24 @@ function ShortcutsSection( { selectedSite }: Pick< ContentTabOverviewProps, 'sel } }, } ); - const phpMyAdminButton = selectedSite.enablePhpMyAdmin ? ( - -
- -
-
- ) : null; - return ( -
- - { phpMyAdminButton &&
{ phpMyAdminButton }
} -
- ); + buttonsArray.push( { + label: __( 'phpMyAdmin' ), + className: 'text-nowrap', + icon: grid, + disabled: ! selectedSite.enablePhpMyAdmin || isServerLoading || isOffline, + onClick: async () => { + if ( ! selectedSite.running ) { + await startServer( selectedSite ); + } + getIpcApi().openSiteURL( + selectedSite.id, + '/phpmyadmin/index.php?route=/database/structure&db=wordpress' + ); + }, + } ); + + return ; } export function ContentTabOverview( { selectedSite }: ContentTabOverviewProps ) { From 17e853460b6369d0f080d0affa30c89e4425610e Mon Sep 17 00:00:00 2001 From: Wojtek Naruniec Date: Tue, 17 Mar 2026 16:54:52 +0100 Subject: [PATCH 05/13] Fix phpMyAdmin checkbox layout to match other debug options --- .../src/modules/site-settings/edit-site-details.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/studio/src/modules/site-settings/edit-site-details.tsx b/apps/studio/src/modules/site-settings/edit-site-details.tsx index 6286f8b1c5..860eaa13f3 100644 --- a/apps/studio/src/modules/site-settings/edit-site-details.tsx +++ b/apps/studio/src/modules/site-settings/edit-site-details.tsx @@ -635,12 +635,12 @@ const EditSiteDetails = ( { currentWpVersion, onSave }: EditSiteDetailsProps ) = { __( 'Enable phpMyAdmin' ) } -
- { __( - 'Access phpMyAdmin to browse and manage your site database. Requires internet to download.' - ) } -
+
+ { __( + 'Access phpMyAdmin to browse and manage your site database. Requires internet to download.' + ) } +
) } From 51ef389dea4d39b12686b69c1ad72cd6df5885ba Mon Sep 17 00:00:00 2001 From: Wojtek Naruniec Date: Tue, 17 Mar 2026 16:56:45 +0100 Subject: [PATCH 06/13] Add phpMyAdmin row to Debugging section in Settings tab --- apps/studio/src/components/content-tab-settings.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/studio/src/components/content-tab-settings.tsx b/apps/studio/src/components/content-tab-settings.tsx index 87b6ee6532..2f97ac3b82 100644 --- a/apps/studio/src/components/content-tab-settings.tsx +++ b/apps/studio/src/components/content-tab-settings.tsx @@ -182,6 +182,9 @@ export function ContentTabSettings( { selectedSite }: ContentTabSettingsProps ) { selectedSite.enableDebugDisplay ? __( 'Enabled' ) : __( 'Disabled' ) } + + { selectedSite.enablePhpMyAdmin ? __( 'Enabled' ) : __( 'Disabled' ) } + From 2b0193eac9a920d9424dd36dff09b96d0ce8cf8c Mon Sep 17 00:00:00 2001 From: Wojtek Naruniec Date: Tue, 17 Mar 2026 17:01:56 +0100 Subject: [PATCH 07/13] Patch @wp-playground/cli to allow --phpmyadmin with --skip-sqlite-setup --- .../patches/@wp-playground+cli+3.1.12.patch | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 apps/cli/patches/@wp-playground+cli+3.1.12.patch diff --git a/apps/cli/patches/@wp-playground+cli+3.1.12.patch b/apps/cli/patches/@wp-playground+cli+3.1.12.patch new file mode 100644 index 0000000000..375ef92809 --- /dev/null +++ b/apps/cli/patches/@wp-playground+cli+3.1.12.patch @@ -0,0 +1,29 @@ +diff --git a/node_modules/@wp-playground/cli/run-cli-2YzKNrNz.js b/node_modules/@wp-playground/cli/run-cli-2YzKNrNz.js +index 1314a71..92c2e31 100644 +--- a/node_modules/@wp-playground/cli/run-cli-2YzKNrNz.js ++++ b/node_modules/@wp-playground/cli/run-cli-2YzKNrNz.js +@@ -1259,10 +1259,7 @@ async function Lt(e) { + w.setSeverityFilterLevel(c); + } + if (e.intl || (e.intl = !0), e.redis === void 0 && (e.redis = await ie()), e.memcached === void 0 && (e.memcached = await ie()), e.phpmyadmin) { +- if (e.phpmyadmin === !0 && (e.phpmyadmin = "/phpmyadmin"), e.skipSqliteSetup) +- throw new Error( +- "--phpmyadmin requires SQLite. Cannot be used with --skip-sqlite-setup." +- ); ++ if (e.phpmyadmin === !0 && (e.phpmyadmin = "/phpmyadmin")); + e.pathAliases = [ + { + urlPrefix: e.phpmyadmin, +diff --git a/node_modules/@wp-playground/cli/run-cli-C-eCY5Ux.cjs b/node_modules/@wp-playground/cli/run-cli-C-eCY5Ux.cjs +index 9b7bc95..e16e51d 100644 +--- a/node_modules/@wp-playground/cli/run-cli-C-eCY5Ux.cjs ++++ b/node_modules/@wp-playground/cli/run-cli-C-eCY5Ux.cjs +@@ -44,7 +44,7 @@ Examples: + wp-playground start --wp=6.7 --php=8.3 # Use specific versions + wp-playground start --skip-browser # Skip opening browser + wp-playground start --no-auto-mount # Disable auto-detection`).options(o)).command("server","Start a local WordPress server (advanced, low-level)",i=>i.options({...t,...r})).command("run-blueprint","Execute a Blueprint without starting a server",i=>i.options({...t})).command("build-snapshot","Build a ZIP snapshot of a WordPress site based on a Blueprint",i=>i.options({...t,...s})).command("php","Run a PHP script",i=>i.options({...t})).demandCommand(1,"Please specify a command").strictCommands().conflicts("experimental-unsafe-ide-integration","experimental-devtools").showHelpOnFail(!1).fail((i,h,g)=>{if(h)throw h;i&&i.includes("Please specify a command")&&(g.showHelp(),console.error(` +-`+i),process.exit(1)),console.error(i),process.exit(1)}).strictOptions().check(async i=>{if(i["skip-wordpress-install"]===!0&&(i["wordpress-install-mode"]="do-not-attempt-installing",i.wordpressInstallMode="do-not-attempt-installing"),i.wp!==void 0&&typeof i.wp=="string"&&!je(i.wp))try{new URL(i.wp)}catch{throw new Error('Unrecognized WordPress version. Please use "latest", a URL, or a numeric version such as "6.2", "6.0.1", "6.2-beta1", or "6.2-RC1"')}const h=i["site-url"];if(typeof h=="string"&&h.trim()!=="")try{new URL(h)}catch{throw new Error(`Invalid site-url "${h}". Please provide a valid URL (e.g., http://localhost:8080 or https://example.com)`)}if(i["auto-mount"]){let g=!1;try{g=c.statSync(i["auto-mount"]).isDirectory()}catch{g=!1}if(!g)throw new Error(`The specified --auto-mount path is not a directory: '${i["auto-mount"]}'.`)}if(i["experimental-blueprints-v2-runner"]===!0)throw new Error("Blueprints v2 are temporarily disabled while we rework their runtime implementation.");if(i.mode!==void 0)throw new Error("The --mode option requires the --experimentalBlueprintsV2Runner flag.");return!0});n.wrap(n.terminalWidth());const a=await n.argv,l=a._[0];["start","run-blueprint","server","build-snapshot","php"].includes(l)||(n.showHelp(),process.exit(1));const u=a.define||{};!("WP_DEBUG"in u)&&!("WP_DEBUG_LOG"in u)&&!("WP_DEBUG_DISPLAY"in u)&&(u.WP_DEBUG="true",u.WP_DEBUG_LOG="true",u.WP_DEBUG_DISPLAY="true");const b={...a,define:u,command:l,mount:[...a.mount||[],...a["mount-dir"]||[]],"mount-before-install":[...a["mount-before-install"]||[],...a["mount-dir-before-install"]||[]]},T=await fe(b);T===void 0&&process.exit(0);const p=(()=>{let i;return async()=>{i===void 0&&(i=T[Symbol.asyncDispose]()),await i,process.exit(0)}})();return process.on("SIGINT",p),process.on("SIGTERM",p),{[Symbol.asyncDispose]:async()=>{process.off("SIGINT",p),process.off("SIGTERM",p),await T[Symbol.asyncDispose]()},[K]:{cliServer:T}}}catch(t){if(console.error(t),!(t instanceof Error))throw t;if(process.argv.includes("--debug"))y.printDebugDetails(t);else{const o=[];let s=t;do o.push(s.message),s=s.cause;while(s instanceof Error);console.error("\x1B[1m"+o.join(" caused by: ")+"\x1B[0m")}process.exit(1)}}function le(e,t){return e.find(r=>r.vfsPath.replace(/\/$/,"")===t.replace(/\/$/,""))}const K=Symbol("playground-cli-testing"),C=e=>process.stdout.isTTY?"\x1B[1m"+e+"\x1B[0m":e,nt=e=>process.stdout.isTTY?"\x1B[31m"+e+"\x1B[0m":e,ue=e=>process.stdout.isTTY?`\x1B[2m${e}\x1B[0m`:e,H=e=>process.stdout.isTTY?`\x1B[3m${e}\x1B[0m`:e,z=e=>process.stdout.isTTY?`\x1B[33m${e}\x1B[0m`:e;async function fe(e){let t;const r=e.internalCookieStore?new y.HttpCookieStore:void 0,o=[],s=new Map;if(e.command==="start"&&(e=it(e)),e.autoMount!==void 0&&(e.autoMount===""&&(e={...e,autoMount:process.cwd()}),e=pe(e)),e.wordpressInstallMode===void 0&&(e.wordpressInstallMode="download-and-install"),e.quiet&&(e.verbosity="quiet",delete e.quiet),e.debug&&(e.verbosity="debug",delete e.debug),e.verbosity){const p=Object.values(J).find(i=>i.name===e.verbosity).severity;m.logger.setSeverityFilterLevel(p)}if(e.intl||(e.intl=!0),e.redis===void 0&&(e.redis=await ne.jspi()),e.memcached===void 0&&(e.memcached=await ne.jspi()),e.phpmyadmin){if(e.phpmyadmin===!0&&(e.phpmyadmin="/phpmyadmin"),e.skipSqliteSetup)throw new Error("--phpmyadmin requires SQLite. Cannot be used with --skip-sqlite-setup.");e.pathAliases=[{urlPrefix:e.phpmyadmin,fsPath:_.PHPMYADMIN_INSTALL_PATH}]}const n=new ot({verbosity:e.verbosity||"normal"});e.command==="server"&&(n.printBanner(),n.printConfig({phpVersion:e.php||B.RecommendedPHPVersion,wpVersion:e.wp||"latest",port:e.port??9400,xdebug:!!e.xdebug,intl:!!e.intl,redis:!!e.redis,memcached:!!e.memcached,mounts:[...e.mount||[],...e["mount-before-install"]||[]],blueprint:typeof e.blueprint=="string"?e.blueprint:void 0}));const a=e.command==="server"?e.port??9400:0,l=new y.FileLockManagerInMemory;let u=!1,b=!0;const T=await Ne({port:e.port?e.port:await He(a)?0:a,onBind:async(p,i)=>{const h="127.0.0.1",g=`http://${h}:${i}`,F=e["site-url"]||g,O=6,ee="-playground-cli-site-",R=await Je(ee);m.logger.debug(`Native temp dir for VFS root: ${R.path}`);const L="WP Playground CLI - Listen for Xdebug",te=".playground-xdebug-root",q=d.join(process.cwd(),te);if(await A.removeTempDirSymlink(q),e.xdebug){const f={hostPath:d.join(".",d.sep,te),vfsPath:"/"};if(y.SupportedPHPVersions.indexOf(e.php||B.RecommendedPHPVersion)<=y.SupportedPHPVersions.indexOf("8.5"))await A.createTempDirSymlink(R.path,q,process.platform),e.xdebug=A.makeXdebugConfig({cwd:process.cwd(),mounts:[f,...e["mount-before-install"]||[],...e.mount||[]],pathSkippings:["/dev/","/home/","/internal/","/request/","/proc/"]}),console.log(C("Xdebug configured successfully")),console.log(z("Playground source root: ")+".playground-xdebug-root"+H(ue(" – you can set breakpoints and preview Playground's VFS structure in there.")));else if(e.experimentalUnsafeIdeIntegration){await A.createTempDirSymlink(R.path,q,process.platform);try{await A.clearXdebugIDEConfig(L,process.cwd());const w=typeof e.xdebug=="object"?e.xdebug:{},P=await A.addXdebugIDEConfig({name:L,host:h,port:i,ides:e.experimentalUnsafeIdeIntegration,cwd:process.cwd(),mounts:[f,...e["mount-before-install"]||[],...e.mount||[]],ideKey:w.ideKey||"WPPLAYGROUNDCLI"}),S=e.experimentalUnsafeIdeIntegration,x=S.includes("vscode"),E=S.includes("phpstorm"),U=Object.values(P);console.log(""),U.length>0?(console.log(C("Xdebug configured successfully")),console.log(z("Updated IDE config: ")+U.join(" ")),console.log(z("Playground source root: ")+".playground-xdebug-root"+H(ue(" – you can set breakpoints and preview Playground's VFS structure in there.")))):(console.log(C("Xdebug configuration failed.")),console.log("No IDE-specific project settings directory was found in the current working directory.")),console.log(""),x&&P.vscode&&(console.log(C("VS Code / Cursor instructions:")),console.log(" 1. Ensure you have installed an IDE extension for PHP Debugging"),console.log(` (The ${C("PHP Debug")} extension by ${C("Xdebug")} has been a solid option)`),console.log(" 2. Open the Run and Debug panel on the left sidebar"),console.log(` 3. Select "${H(L)}" from the dropdown`),console.log(' 3. Click "start debugging"'),console.log(" 5. Set a breakpoint. For example, in .playground-xdebug-root/wordpress/index.php"),console.log(" 6. Visit Playground in your browser to hit the breakpoint"),E&&console.log("")),E&&P.phpstorm&&(console.log(C("PhpStorm instructions:")),console.log(` 1. Choose "${H(L)}" debug configuration in the toolbar`),console.log(" 2. Click the debug button (bug icon)`"),console.log(" 3. Set a breakpoint. For example, in .playground-xdebug-root/wordpress/index.php"),console.log(" 4. Visit Playground in your browser to hit the breakpoint")),console.log("")}catch(w){throw new Error("Could not configure Xdebug",{cause:w})}}}const ye=d.dirname(R.path),be=2*24*60*60*1e3;Ke(ee,be,ye);const re=d.join(R.path,"internal");c.mkdirSync(re);const ge=["wordpress","tools","tmp","home"];for(const f of ge){const v=P=>P.vfsPath===`/${f}`;if(!(e["mount-before-install"]?.some(v)||e.mount?.some(v))){const P=d.join(R.path,f);c.mkdirSync(P),e["mount-before-install"]===void 0&&(e["mount-before-install"]=[]),e["mount-before-install"].unshift({vfsPath:`/${f}`,hostPath:P})}}if(e["mount-before-install"])for(const f of e["mount-before-install"])m.logger.debug(`Mount before WP install: ${f.vfsPath} -> ${f.hostPath}`);if(e.mount)for(const f of e.mount)m.logger.debug(`Mount after WP install: ${f.vfsPath} -> ${f.hostPath}`);let M;e["experimental-blueprints-v2-runner"]?M=new ze(e,{siteUrl:F,cliOutput:n}):(M=new Ze(e,{siteUrl:F,cliOutput:n}),typeof e.blueprint=="string"&&(e.blueprint=await Ye({sourceString:e.blueprint,blueprintMayReadAdjacentFiles:e["blueprint-may-read-adjacent-files"]===!0})));let V=!1;const D=async function(){V||(V=!0,await Promise.all(o.map(async v=>{await s.get(v)?.dispose(),await v.worker.terminate()})),p&&await new Promise(v=>{p.close(v),p.closeAllConnections()}),await R.cleanup())};try{const f=[],v=M.getWorkerType();for(let w=0;w{V||S===0&&m.logger.error(`Worker ${w} exited with code ${S} ++`+i),process.exit(1)),console.error(i),process.exit(1)}).strictOptions().check(async i=>{if(i["skip-wordpress-install"]===!0&&(i["wordpress-install-mode"]="do-not-attempt-installing",i.wordpressInstallMode="do-not-attempt-installing"),i.wp!==void 0&&typeof i.wp=="string"&&!je(i.wp))try{new URL(i.wp)}catch{throw new Error('Unrecognized WordPress version. Please use "latest", a URL, or a numeric version such as "6.2", "6.0.1", "6.2-beta1", or "6.2-RC1"')}const h=i["site-url"];if(typeof h=="string"&&h.trim()!=="")try{new URL(h)}catch{throw new Error(`Invalid site-url "${h}". Please provide a valid URL (e.g., http://localhost:8080 or https://example.com)`)}if(i["auto-mount"]){let g=!1;try{g=c.statSync(i["auto-mount"]).isDirectory()}catch{g=!1}if(!g)throw new Error(`The specified --auto-mount path is not a directory: '${i["auto-mount"]}'.`)}if(i["experimental-blueprints-v2-runner"]===!0)throw new Error("Blueprints v2 are temporarily disabled while we rework their runtime implementation.");if(i.mode!==void 0)throw new Error("The --mode option requires the --experimentalBlueprintsV2Runner flag.");return!0});n.wrap(n.terminalWidth());const a=await n.argv,l=a._[0];["start","run-blueprint","server","build-snapshot","php"].includes(l)||(n.showHelp(),process.exit(1));const u=a.define||{};!("WP_DEBUG"in u)&&!("WP_DEBUG_LOG"in u)&&!("WP_DEBUG_DISPLAY"in u)&&(u.WP_DEBUG="true",u.WP_DEBUG_LOG="true",u.WP_DEBUG_DISPLAY="true");const b={...a,define:u,command:l,mount:[...a.mount||[],...a["mount-dir"]||[]],"mount-before-install":[...a["mount-before-install"]||[],...a["mount-dir-before-install"]||[]]},T=await fe(b);T===void 0&&process.exit(0);const p=(()=>{let i;return async()=>{i===void 0&&(i=T[Symbol.asyncDispose]()),await i,process.exit(0)}})();return process.on("SIGINT",p),process.on("SIGTERM",p),{[Symbol.asyncDispose]:async()=>{process.off("SIGINT",p),process.off("SIGTERM",p),await T[Symbol.asyncDispose]()},[K]:{cliServer:T}}}catch(t){if(console.error(t),!(t instanceof Error))throw t;if(process.argv.includes("--debug"))y.printDebugDetails(t);else{const o=[];let s=t;do o.push(s.message),s=s.cause;while(s instanceof Error);console.error("\x1B[1m"+o.join(" caused by: ")+"\x1B[0m")}process.exit(1)}}function le(e,t){return e.find(r=>r.vfsPath.replace(/\/$/,"")===t.replace(/\/$/,""))}const K=Symbol("playground-cli-testing"),C=e=>process.stdout.isTTY?"\x1B[1m"+e+"\x1B[0m":e,nt=e=>process.stdout.isTTY?"\x1B[31m"+e+"\x1B[0m":e,ue=e=>process.stdout.isTTY?`\x1B[2m${e}\x1B[0m`:e,H=e=>process.stdout.isTTY?`\x1B[3m${e}\x1B[0m`:e,z=e=>process.stdout.isTTY?`\x1B[33m${e}\x1B[0m`:e;async function fe(e){let t;const r=e.internalCookieStore?new y.HttpCookieStore:void 0,o=[],s=new Map;if(e.command==="start"&&(e=it(e)),e.autoMount!==void 0&&(e.autoMount===""&&(e={...e,autoMount:process.cwd()}),e=pe(e)),e.wordpressInstallMode===void 0&&(e.wordpressInstallMode="download-and-install"),e.quiet&&(e.verbosity="quiet",delete e.quiet),e.debug&&(e.verbosity="debug",delete e.debug),e.verbosity){const p=Object.values(J).find(i=>i.name===e.verbosity).severity;m.logger.setSeverityFilterLevel(p)}if(e.intl||(e.intl=!0),e.redis===void 0&&(e.redis=await ne.jspi()),e.memcached===void 0&&(e.memcached=await ne.jspi()),e.phpmyadmin){if(e.phpmyadmin===!0)e.phpmyadmin="/phpmyadmin";e.pathAliases=[{urlPrefix:e.phpmyadmin,fsPath:_.PHPMYADMIN_INSTALL_PATH}]}const n=new ot({verbosity:e.verbosity||"normal"});e.command==="server"&&(n.printBanner(),n.printConfig({phpVersion:e.php||B.RecommendedPHPVersion,wpVersion:e.wp||"latest",port:e.port??9400,xdebug:!!e.xdebug,intl:!!e.intl,redis:!!e.redis,memcached:!!e.memcached,mounts:[...e.mount||[],...e["mount-before-install"]||[]],blueprint:typeof e.blueprint=="string"?e.blueprint:void 0}));const a=e.command==="server"?e.port??9400:0,l=new y.FileLockManagerInMemory;let u=!1,b=!0;const T=await Ne({port:e.port?e.port:await He(a)?0:a,onBind:async(p,i)=>{const h="127.0.0.1",g=`http://${h}:${i}`,F=e["site-url"]||g,O=6,ee="-playground-cli-site-",R=await Je(ee);m.logger.debug(`Native temp dir for VFS root: ${R.path}`);const L="WP Playground CLI - Listen for Xdebug",te=".playground-xdebug-root",q=d.join(process.cwd(),te);if(await A.removeTempDirSymlink(q),e.xdebug){const f={hostPath:d.join(".",d.sep,te),vfsPath:"/"};if(y.SupportedPHPVersions.indexOf(e.php||B.RecommendedPHPVersion)<=y.SupportedPHPVersions.indexOf("8.5"))await A.createTempDirSymlink(R.path,q,process.platform),e.xdebug=A.makeXdebugConfig({cwd:process.cwd(),mounts:[f,...e["mount-before-install"]||[],...e.mount||[]],pathSkippings:["/dev/","/home/","/internal/","/request/","/proc/"]}),console.log(C("Xdebug configured successfully")),console.log(z("Playground source root: ")+".playground-xdebug-root"+H(ue(" – you can set breakpoints and preview Playground's VFS structure in there.")));else if(e.experimentalUnsafeIdeIntegration){await A.createTempDirSymlink(R.path,q,process.platform);try{await A.clearXdebugIDEConfig(L,process.cwd());const w=typeof e.xdebug=="object"?e.xdebug:{},P=await A.addXdebugIDEConfig({name:L,host:h,port:i,ides:e.experimentalUnsafeIdeIntegration,cwd:process.cwd(),mounts:[f,...e["mount-before-install"]||[],...e.mount||[]],ideKey:w.ideKey||"WPPLAYGROUNDCLI"}),S=e.experimentalUnsafeIdeIntegration,x=S.includes("vscode"),E=S.includes("phpstorm"),U=Object.values(P);console.log(""),U.length>0?(console.log(C("Xdebug configured successfully")),console.log(z("Updated IDE config: ")+U.join(" ")),console.log(z("Playground source root: ")+".playground-xdebug-root"+H(ue(" – you can set breakpoints and preview Playground's VFS structure in there.")))):(console.log(C("Xdebug configuration failed.")),console.log("No IDE-specific project settings directory was found in the current working directory.")),console.log(""),x&&P.vscode&&(console.log(C("VS Code / Cursor instructions:")),console.log(" 1. Ensure you have installed an IDE extension for PHP Debugging"),console.log(` (The ${C("PHP Debug")} extension by ${C("Xdebug")} has been a solid option)`),console.log(" 2. Open the Run and Debug panel on the left sidebar"),console.log(` 3. Select "${H(L)}" from the dropdown`),console.log(' 3. Click "start debugging"'),console.log(" 5. Set a breakpoint. For example, in .playground-xdebug-root/wordpress/index.php"),console.log(" 6. Visit Playground in your browser to hit the breakpoint"),E&&console.log("")),E&&P.phpstorm&&(console.log(C("PhpStorm instructions:")),console.log(` 1. Choose "${H(L)}" debug configuration in the toolbar`),console.log(" 2. Click the debug button (bug icon)`"),console.log(" 3. Set a breakpoint. For example, in .playground-xdebug-root/wordpress/index.php"),console.log(" 4. Visit Playground in your browser to hit the breakpoint")),console.log("")}catch(w){throw new Error("Could not configure Xdebug",{cause:w})}}}const ye=d.dirname(R.path),be=2*24*60*60*1e3;Ke(ee,be,ye);const re=d.join(R.path,"internal");c.mkdirSync(re);const ge=["wordpress","tools","tmp","home"];for(const f of ge){const v=P=>P.vfsPath===`/${f}`;if(!(e["mount-before-install"]?.some(v)||e.mount?.some(v))){const P=d.join(R.path,f);c.mkdirSync(P),e["mount-before-install"]===void 0&&(e["mount-before-install"]=[]),e["mount-before-install"].unshift({vfsPath:`/${f}`,hostPath:P})}}if(e["mount-before-install"])for(const f of e["mount-before-install"])m.logger.debug(`Mount before WP install: ${f.vfsPath} -> ${f.hostPath}`);if(e.mount)for(const f of e.mount)m.logger.debug(`Mount after WP install: ${f.vfsPath} -> ${f.hostPath}`);let M;e["experimental-blueprints-v2-runner"]?M=new ze(e,{siteUrl:F,cliOutput:n}):(M=new Ze(e,{siteUrl:F,cliOutput:n}),typeof e.blueprint=="string"&&(e.blueprint=await Ye({sourceString:e.blueprint,blueprintMayReadAdjacentFiles:e["blueprint-may-read-adjacent-files"]===!0})));let V=!1;const D=async function(){V||(V=!0,await Promise.all(o.map(async v=>{await s.get(v)?.dispose(),await v.worker.terminate()})),p&&await new Promise(v=>{p.close(v),p.closeAllConnections()}),await R.cleanup())};try{const f=[],v=M.getWorkerType();for(let w=0;w{V||S===0&&m.logger.error(`Worker ${w} exited with code ${S} + `)}}).then(async S=>{o.push(S);const x=await at(l),E=await M.bootRequestHandler({worker:S,fileLockManagerPort:x,nativeInternalDirPath:re});return s.set(S,E),[S,E]});f.push(P),w===0&&await P}await Promise.all(f),t=y.createObjectPoolProxy(o.map(w=>s.get(w)));{const w=new N.MessageChannel,P=w.port1,S=w.port2;if(await y.exposeAPI({applyPostInstallMountsToAllWorkers:async()=>{await Promise.all(Array.from(s.values()).map(x=>x.mountAfterWordPressInstall(e.mount||[])))}},void 0,P),await M.bootWordPress(t,S),P.close(),u=!0,!e["experimental-blueprints-v2-runner"]){const x=await M.compileInputBlueprint(e["additional-blueprint-steps"]||[]);x&&await I.runBlueprintV1Steps(x,t)}if(e.phpmyadmin&&!await t.fileExists(`${_.PHPMYADMIN_INSTALL_PATH}/index.php`)){const x=await _.getPhpMyAdminInstallSteps(),E=await I.compileBlueprintV1({steps:x});await I.runBlueprintV1Steps(E,t)}if(e.command==="build-snapshot"){await ut(t,e.outfile),n.printStatus(`Exported to ${e.outfile}`),await D();return}else if(e.command==="run-blueprint"){n.finishProgress("Done"),await D();return}else if(e.command==="php"){const x=["/internal/shared/bin/php",...(e._||[]).slice(1)],E=await t.cli(x),[U]=await Promise.all([E.exitCode,E.stdout.pipeTo(new WritableStream({write(j){process.stdout.write(j)}})),E.stderr.pipeTo(new WritableStream({write(j){process.stderr.write(j)}}))]);await D(),process.exit(U)}}if(n.finishProgress(),n.printReady(g,O),e.phpmyadmin){const w=d.join(e.phpmyadmin,_.PHPMYADMIN_ENTRY_PATH);n.printPhpMyAdminUrl(new URL(w,g).toString())}return e.xdebug&&e.experimentalDevtools&&(await Ie.startBridge({phpInstance:t,phpRoot:"/wordpress"})).start(),{playground:t,server:p,serverUrl:g,[Symbol.asyncDispose]:D,[K]:{workerThreadCount:O}}}catch(f){if(e.verbosity!=="debug")throw f;let v="";throw await t?.fileExists(m.errorLogPath)&&(v=await t.readFileAsText(m.errorLogPath)),await D(),new Error(v,{cause:f})}},async handleRequest(p){if(!u)return y.StreamedPHPResponse.forHttpCode(502,"WordPress is not ready yet");if(b){b=!1;const h={"Content-Type":["text/plain"],"Content-Length":["0"],Location:[p.url]};return p.headers?.cookie?.includes("playground_auto_login_already_happened")&&(h["Set-Cookie"]=["playground_auto_login_already_happened=1; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Path=/"]),y.StreamedPHPResponse.fromPHPResponse(new y.PHPResponse(302,h,new Uint8Array))}r&&(p={...p,headers:{...p.headers,cookie:r.getCookieRequestHeader()}});const i=await t.requestStreamed(p);if(r){const h=await i.headers;r.rememberCookiesFromResponseHeaders(h),delete h["set-cookie"]}return i}}).catch(p=>{n.printError(p.message),process.exit(1)});return T&&e.command==="start"&&!e.skipBrowser&<(T.serverUrl),T}function it(e){let t={...e,command:"server"};e.noAutoMount||(t.autoMount=d.resolve(process.cwd(),t.path??""),t=pe(t),delete t.autoMount);const r=le(t["mount-before-install"]||[],"/wordpress")||le(t.mount||[],"/wordpress");if(r)console.log("Site files stored at:",r?.hostPath),e.reset&&(console.log(""),console.log(nt("This site is not managed by Playground CLI and cannot be reset.")),console.log("(It's not stored in the ~/.wordpress-playground/sites/ directory.)"),console.log(""),console.log("You may still remove the site's directory manually if you wish."),process.exit(1));else{const o=t.autoMount||process.cwd(),s=ke.createHash("sha256").update(o).digest("hex"),n=X.homedir(),a=d.join(n,".wordpress-playground/sites",s);console.log("Site files stored at:",a),c.existsSync(a)&&e.reset&&(console.log("Resetting site..."),c.rmdirSync(a,{recursive:!0})),c.mkdirSync(a,{recursive:!0}),t["mount-before-install"]=[...t["mount-before-install"]||[],{vfsPath:"/wordpress",hostPath:a}],t.wordpressInstallMode=c.readdirSync(a).length===0?"download-and-install":"install-from-existing-files-if-needed"}return t}const G=new y.ProcessIdAllocator;function we(e,{onExit:t}={}){let r;return e==="v1"?r=new N.Worker(new URL("./worker-thread-v1.cjs",typeof document>"u"?require("url").pathToFileURL(__filename).href:$&&$.tagName.toUpperCase()==="SCRIPT"&&$.src||new URL("run-cli-C-eCY5Ux.cjs",document.baseURI).href)):r=new N.Worker(new URL("./worker-thread-v2.cjs",typeof document>"u"?require("url").pathToFileURL(__filename).href:$&&$.tagName.toUpperCase()==="SCRIPT"&&$.src||new URL("run-cli-C-eCY5Ux.cjs",document.baseURI).href)),new Promise((o,s)=>{const n=G.claim();r.once("message",function(l){l.command==="worker-script-initialized"&&o({processId:n,worker:r,phpPort:l.phpPort})}),r.once("error",function(l){G.release(n),console.error(l);const u=new Error(`Worker failed to load worker. ${l.message?`Original error: ${l.message}`:""}`);s(u)});let a=!1;r.once("spawn",()=>{a=!0}),r.once("exit",l=>{G.release(n),a||s(new Error(`Worker exited before spawning: ${l}`)),t?.(l)})})}async function at(e){const{port1:t,port2:r}=new N.MessageChannel;return await y.exposeSyncAPI(e,t),r}function lt(e){const t=X.platform();let r;switch(t){case"darwin":r=`open "${e}"`;break;case"win32":r=`start "" "${e}"`;break;default:r=`xdg-open "${e}"`;break}de.exec(r,o=>{o&&m.logger.debug(`Could not open browser: ${o.message}`)})}async function ut(e,t){await e.run({code:`open('/tmp/build.zip', ZipArchive::CREATE | ZipArchive::OVERWRITE)) { From 78f37addd751eea3b16f2baf0f6e37403bf879ff Mon Sep 17 00:00:00 2001 From: Wojtek Naruniec Date: Tue, 17 Mar 2026 17:06:10 +0100 Subject: [PATCH 08/13] Fix phpMyAdmin not preserved during site restart via blueprint path --- apps/cli/lib/wordpress-server-manager.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/cli/lib/wordpress-server-manager.ts b/apps/cli/lib/wordpress-server-manager.ts index 5ce92d72e7..c6a8790ef2 100644 --- a/apps/cli/lib/wordpress-server-manager.ts +++ b/apps/cli/lib/wordpress-server-manager.ts @@ -370,6 +370,10 @@ export async function runBlueprint( serverConfig.enableDebugDisplay = true; } + if ( site.enablePhpMyAdmin ) { + serverConfig.enablePhpMyAdmin = true; + } + const processDesc = await startProcess( processName, wordPressServerChildPath ); try { await waitForReadyMessage( processDesc.pmId ); From 5e7c6ab34ba6b1e481a85c1ccae89c61a1c6180e Mon Sep 17 00:00:00 2001 From: Wojtek Naruniec Date: Tue, 17 Mar 2026 17:09:56 +0100 Subject: [PATCH 09/13] Fix enablePhpMyAdmin not persisted to disk in Studio appdata --- apps/studio/src/storage/user-data.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/studio/src/storage/user-data.ts b/apps/studio/src/storage/user-data.ts index 6ca0ca6985..cfa849c1df 100644 --- a/apps/studio/src/storage/user-data.ts +++ b/apps/studio/src/storage/user-data.ts @@ -201,6 +201,7 @@ function toDiskFormat( { sites, ...rest }: UserData ): PersistedUserData { enableXdebug, enableDebugLog, enableDebugDisplay, + enablePhpMyAdmin, sortOrder, } ) => { // No object spreading allowed. TypeScript's structural typing is too permissive and @@ -223,6 +224,7 @@ function toDiskFormat( { sites, ...rest }: UserData ): PersistedUserData { enableXdebug, enableDebugLog, enableDebugDisplay, + enablePhpMyAdmin, sortOrder, themeDetails: { name: themeDetails?.name || '', From 6fb6b87a84c7cdf4fdea94f66e8349fe5bdd8bfc Mon Sep 17 00:00:00 2001 From: Wojtek Naruniec Date: Tue, 17 Mar 2026 17:31:55 +0100 Subject: [PATCH 10/13] Update set.test.ts expected error message to include --phpmyadmin --- apps/cli/commands/site/tests/set.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/cli/commands/site/tests/set.test.ts b/apps/cli/commands/site/tests/set.test.ts index 2d72ae6d6a..4893bf913a 100644 --- a/apps/cli/commands/site/tests/set.test.ts +++ b/apps/cli/commands/site/tests/set.test.ts @@ -100,7 +100,7 @@ describe( 'CLI: studio site set', () => { describe( 'Validation', () => { it( 'should throw when no options provided', async () => { await expect( runCommand( testSitePath, {} ) ).rejects.toThrow( - 'At least one option (--name, --domain, --https, --php, --wp, --xdebug, --admin-username, --admin-password, --admin-email, --debug-log, --debug-display) is required.' + 'At least one option (--name, --domain, --https, --php, --wp, --xdebug, --admin-username, --admin-password, --admin-email, --debug-log, --debug-display, --phpmyadmin) is required.' ); } ); From 5aa9b356c10f08869f662675544d9ef5d140e240 Mon Sep 17 00:00:00 2001 From: Wojtek Naruniec Date: Tue, 17 Mar 2026 17:33:06 +0100 Subject: [PATCH 11/13] Update content-tab-settings test to expect 5 Disabled rows --- .../studio/src/components/tests/content-tab-settings.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/studio/src/components/tests/content-tab-settings.test.tsx b/apps/studio/src/components/tests/content-tab-settings.test.tsx index fbd3e3bee1..6134e6f2e8 100644 --- a/apps/studio/src/components/tests/content-tab-settings.test.tsx +++ b/apps/studio/src/components/tests/content-tab-settings.test.tsx @@ -158,8 +158,8 @@ describe( 'ContentTabSettings', () => { ).toHaveTextContent( 'localhost:8881' ); expect( screen.getByText( 'HTTPS' ) ).toBeVisible(); expect( screen.getByText( 'Xdebug' ) ).toBeVisible(); - // HTTPS, Xdebug, Debug log, and Debug display show "Disabled" - expect( screen.getAllByText( 'Disabled' ) ).toHaveLength( 4 ); + // HTTPS, Xdebug, Debug log, Debug display, and phpMyAdmin show "Disabled" + expect( screen.getAllByText( 'Disabled' ) ).toHaveLength( 5 ); expect( screen.getByRole( 'button', { name: 'Copy local path to clipboard' } ) ).toBeVisible(); expect( screen.getByText( '7.7.7' ) ).toBeVisible(); expect( From 558790632e8aefe260a49b2bbeb57cbff3804cd8 Mon Sep 17 00:00:00 2001 From: Wojtek Naruniec Date: Tue, 17 Mar 2026 17:42:58 +0100 Subject: [PATCH 12/13] Patch @wp-playground/tools DbiMysqli to use Studio's SQLite path --- apps/cli/patches/@wp-playground+tools+3.1.12.patch | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 apps/cli/patches/@wp-playground+tools+3.1.12.patch diff --git a/apps/cli/patches/@wp-playground+tools+3.1.12.patch b/apps/cli/patches/@wp-playground+tools+3.1.12.patch new file mode 100644 index 0000000000..edcd04069d --- /dev/null +++ b/apps/cli/patches/@wp-playground+tools+3.1.12.patch @@ -0,0 +1,13 @@ +diff --git a/node_modules/@wp-playground/tools/DbiMysqli-CmlcCdDi.js b/node_modules/@wp-playground/tools/DbiMysqli-CmlcCdDi.js +index f17cbe9..a8ac6d7 100644 +--- a/node_modules/@wp-playground/tools/DbiMysqli-CmlcCdDi.js ++++ b/node_modules/@wp-playground/tools/DbiMysqli-CmlcCdDi.js +@@ -23,7 +23,7 @@ use WP_SQLite_Connection; + use WP_SQLite_Driver; + + // Load the SQLite driver. +-require_once '/internal/shared/sqlite-database-integration/wp-pdo-mysql-on-sqlite.php'; ++require_once '/wordpress/wp-content/mu-plugins/sqlite-database-integration/wp-pdo-mysql-on-sqlite.php'; + + // Supress the following phpMyAdmin warning: + // "The mysqlnd extension is missing. Please check your PHP configuration." From f6e42bb399fcb8eddf7fcdaef68953a7498ee098 Mon Sep 17 00:00:00 2001 From: Wojtek Naruniec Date: Wed, 18 Mar 2026 10:07:32 +0100 Subject: [PATCH 13/13] Bundle phpMyAdmin at build time for offline support Download phpMyAdmin at build time (like sqlite-database-integration), inject Playground-specific config and SQLite adapter, copy to server-files on app start, and mount into the VFS at /tools/phpmyadmin when starting a site. Playground skips downloading if the directory already exists in the VFS, enabling offline use. Remove the useOffline() guards from the phpMyAdmin UI elements since an internet connection is no longer required. Co-Authored-By: Claude Sonnet 4.5 --- apps/cli/lib/server-files.ts | 4 ++ apps/cli/wordpress-server-child.ts | 11 ++++- .../src/components/content-tab-overview.tsx | 4 +- apps/studio/src/lib/server-files-paths.ts | 7 +++ .../site-settings/edit-site-details.tsx | 48 +++++++------------ apps/studio/src/setup-wp-server-files.ts | 11 +++++ scripts/download-wp-server-files.ts | 44 +++++++++++++++++ 7 files changed, 94 insertions(+), 35 deletions(-) diff --git a/apps/cli/lib/server-files.ts b/apps/cli/lib/server-files.ts index b148eb4733..4d9aef3a49 100644 --- a/apps/cli/lib/server-files.ts +++ b/apps/cli/lib/server-files.ts @@ -23,3 +23,7 @@ export function getLanguagePacksPath(): string { export function getAgentSkillsPath(): string { return path.join( getServerFilesPath(), 'agent-skills' ); } + +export function getPhpMyAdminPath(): string { + return path.join( getServerFilesPath(), 'phpmyadmin' ); +} diff --git a/apps/cli/wordpress-server-child.ts b/apps/cli/wordpress-server-child.ts index 49fc70af22..26d1cf5200 100644 --- a/apps/cli/wordpress-server-child.ts +++ b/apps/cli/wordpress-server-child.ts @@ -27,9 +27,10 @@ import { InMemoryFilesystem, } from '@wp-playground/storage'; import { WordPressInstallMode } from '@wp-playground/wordpress'; +import fs from 'fs-extra'; import { z } from 'zod'; import { sanitizeRunCLIArgs } from 'cli/lib/cli-args-sanitizer'; -import { getSqliteCommandPath, getWpCliPharPath } from 'cli/lib/server-files'; +import { getPhpMyAdminPath, getSqliteCommandPath, getWpCliPharPath } from 'cli/lib/server-files'; import { isSqliteIntegrationInstalled } from 'cli/lib/sqlite-integration'; import { ServerConfig, @@ -261,6 +262,14 @@ async function getBaseRunCLIArgs( } if ( config.enablePhpMyAdmin ) { + const phpMyAdminHostPath = getPhpMyAdminPath(); + if ( await fs.pathExists( phpMyAdminHostPath ) ) { + mounts.push( { + hostPath: phpMyAdminHostPath, + vfsPath: '/tools/phpmyadmin', + } ); + logToConsole( 'Mounting bundled phpMyAdmin' ); + } logToConsole( 'Enabling phpMyAdmin support' ); args.phpmyadmin = true; } diff --git a/apps/studio/src/components/content-tab-overview.tsx b/apps/studio/src/components/content-tab-overview.tsx index 6bda7d61b7..0133bcc9b4 100644 --- a/apps/studio/src/components/content-tab-overview.tsx +++ b/apps/studio/src/components/content-tab-overview.tsx @@ -18,7 +18,6 @@ import { useI18n } from '@wordpress/react-i18n'; import { useState } from 'react'; import { ArrowIcon } from 'src/components/arrow-icon'; import { ButtonsSection, ButtonsSectionProps } from 'src/components/buttons-section'; -import { useOffline } from 'src/hooks/use-offline'; import { useSiteDetails } from 'src/hooks/use-site-details'; import { useThemeDetails } from 'src/hooks/use-theme-details'; import { isWindows } from 'src/lib/app-globals'; @@ -137,7 +136,6 @@ function ShortcutsSection( { selectedSite }: Pick< ContentTabOverviewProps, 'sel const { data: editor } = useGetUserEditorQuery(); const { data: terminal } = useGetUserTerminalQuery(); const { startServer, loadingServer } = useSiteDetails(); - const isOffline = useOffline(); const isServerLoading = loadingServer[ selectedSite.id ]; const buttonsArray: ButtonsSectionProps[ 'buttonsArray' ] = [ @@ -186,7 +184,7 @@ function ShortcutsSection( { selectedSite }: Pick< ContentTabOverviewProps, 'sel label: __( 'phpMyAdmin' ), className: 'text-nowrap', icon: grid, - disabled: ! selectedSite.enablePhpMyAdmin || isServerLoading || isOffline, + disabled: ! selectedSite.enablePhpMyAdmin || isServerLoading, onClick: async () => { if ( ! selectedSite.running ) { await startServer( selectedSite ); diff --git a/apps/studio/src/lib/server-files-paths.ts b/apps/studio/src/lib/server-files-paths.ts index b394931b7d..5e5d226fff 100644 --- a/apps/studio/src/lib/server-files-paths.ts +++ b/apps/studio/src/lib/server-files-paths.ts @@ -78,3 +78,10 @@ export function getLanguagePacksPath(): string { export function getAgentSkillsPath(): string { return path.join( getBasePath(), 'agent-skills' ); } + +/** + * The path where bundled phpMyAdmin files are stored. + */ +export function getPhpMyAdminPath(): string { + return path.join( getBasePath(), 'phpmyadmin' ); +} diff --git a/apps/studio/src/modules/site-settings/edit-site-details.tsx b/apps/studio/src/modules/site-settings/edit-site-details.tsx index 860eaa13f3..5b0289013c 100644 --- a/apps/studio/src/modules/site-settings/edit-site-details.tsx +++ b/apps/studio/src/modules/site-settings/edit-site-details.tsx @@ -24,12 +24,10 @@ import Button from 'src/components/button'; import { ErrorInformation } from 'src/components/error-information'; import { LearnMoreLink, LearnHowLink } from 'src/components/learn-more'; import Modal from 'src/components/modal'; -import offlineIcon from 'src/components/offline-icon'; import PasswordControl from 'src/components/password-control'; import TextControlComponent from 'src/components/text-control'; import { Tooltip } from 'src/components/tooltip'; import { WPVersionSelector } from 'src/components/wp-version-selector'; -import { useOffline } from 'src/hooks/use-offline'; import { useSiteDetails } from 'src/hooks/use-site-details'; import { cx } from 'src/lib/cx'; import { getIpcApi } from 'src/lib/get-ipc-api'; @@ -43,7 +41,6 @@ type EditSiteDetailsProps = { const EditSiteDetails = ( { currentWpVersion, onSave }: EditSiteDetailsProps ) => { const { __ } = useI18n(); const { updateSite, selectedSite, isEditModalOpen, setIsEditModalOpen } = useSiteDetails(); - const isOffline = useOffline(); const [ errorUpdatingWpVersion, setErrorUpdatingWpVersion ] = useState< string | null >( null ); const [ isEditingSite, setIsEditingSite ] = useState( false ); const [ needsRestart, setNeedsRestart ] = useState( false ); @@ -609,37 +606,26 @@ const EditSiteDetails = ( { currentWpVersion, onSave }: EditSiteDetailsProps ) =
- -
- setEnablePhpMyAdmin( e.target.checked ) } - disabled={ isEditingSite || isOffline } - /> - -
-
+
+ setEnablePhpMyAdmin( e.target.checked ) } + disabled={ isEditingSite } + /> + +
- { __( - 'Access phpMyAdmin to browse and manage your site database. Requires internet to download.' - ) } + { __( 'Access phpMyAdmin to browse and manage your site database.' ) }
diff --git a/apps/studio/src/setup-wp-server-files.ts b/apps/studio/src/setup-wp-server-files.ts index 37fead5be9..8f30c51446 100644 --- a/apps/studio/src/setup-wp-server-files.ts +++ b/apps/studio/src/setup-wp-server-files.ts @@ -6,6 +6,7 @@ import { updateLatestWPCliVersion } from 'src/lib/download-utils'; import { getAgentSkillsPath, getLanguagePacksPath, + getPhpMyAdminPath, getWordPressVersionPath, getSqlitePath, getWpCliPath, @@ -122,6 +123,15 @@ async function copyBundledAgentSkills() { await recursiveCopyDirectory( bundledAgentSkillsPath, getAgentSkillsPath() ); } +async function copyBundledPhpMyAdmin() { + const bundledPath = path.join( getResourcesPath(), 'wp-files', 'phpmyadmin' ); + if ( ! ( await fs.pathExists( bundledPath ) ) ) { + return; + } + // Always copy to ensure files are complete and up-to-date + await recursiveCopyDirectory( bundledPath, getPhpMyAdminPath() ); +} + async function copyBundledLanguagePacks() { const bundledLanguagePacksPath = path.join( getResourcesPath(), @@ -146,6 +156,7 @@ export async function setupWPServerFiles() { [ 'translations', copyBundledTranslations ], [ 'language packs', copyBundledLanguagePacks ], [ 'agent skills', copyBundledAgentSkills ], + [ 'phpMyAdmin', copyBundledPhpMyAdmin ], ]; for ( const [ name, step ] of steps ) { diff --git a/scripts/download-wp-server-files.ts b/scripts/download-wp-server-files.ts index 48973a317c..7b33b7657a 100644 --- a/scripts/download-wp-server-files.ts +++ b/scripts/download-wp-server-files.ts @@ -4,6 +4,11 @@ import fs from 'fs-extra'; import { extractZip } from '../tools/common/lib/extract-zip'; import { getLatestSQLiteCommandRelease } from '../apps/studio/src/lib/sqlite-command-release'; import { SQLITE_DATABASE_INTEGRATION_RELEASE_URL } from '../apps/studio/src/constants'; +import { + PHPMYADMIN_DOWNLOAD_URL, + PHPMYADMIN_VERSION, + getPhpMyAdminInstallSteps, +} from '@wp-playground/tools'; const WP_SERVER_FILES_PATH = path.join( __dirname, '..', 'wp-files' ); @@ -42,6 +47,12 @@ const FILES_TO_DOWNLOAD: FileToDownload[] = [ }, destinationPath: path.join( WP_SERVER_FILES_PATH, 'sqlite-command' ), }, + { + name: 'phpmyadmin', + description: 'phpMyAdmin', + getUrl: () => PHPMYADMIN_DOWNLOAD_URL, + destinationPath: path.join( WP_SERVER_FILES_PATH, 'phpmyadmin' ), + }, ]; async function downloadFile( file: FileToDownload ): Promise< void > { @@ -90,6 +101,39 @@ async function downloadFile( file: FileToDownload ): Promise< void > { } fs.renameSync( sourcePath, targetPath ); } + } else if ( name === 'phpmyadmin' ) { + /** + * phpMyAdmin is extracted into a folder like phpMyAdmin-5.2.3-english. + * We extract to a temp dir, rename to the destination, then inject the + * Playground-specific config and SQLite adapter from @wp-playground/tools. + */ + console.log( `[${ name }] Extracting files from zip ...` ); + const tmpExtractPath = path.join( os.tmpdir(), 'phpmyadmin-extract' ); + if ( fs.existsSync( tmpExtractPath ) ) { + fs.rmSync( tmpExtractPath, { recursive: true, force: true } ); + } + await extractZip( zipPath, tmpExtractPath ); + + const innerFolder = `phpMyAdmin-${ PHPMYADMIN_VERSION }-english`; + const sourcePath = path.join( tmpExtractPath, innerFolder ); + if ( fs.existsSync( extractedPath ) ) { + fs.rmSync( extractedPath, { recursive: true, force: true } ); + } + fs.renameSync( sourcePath, extractedPath ); + fs.rmSync( tmpExtractPath, { recursive: true, force: true } ); + + // Inject Playground-specific config and SQLite adapter + console.log( `[${ name }] Injecting Playground-specific files ...` ); + const installSteps = await getPhpMyAdminInstallSteps(); + for ( const step of installSteps ) { + if ( step.step === 'writeFile' && typeof step.data === 'string' ) { + // step.path is like /tools/phpmyadmin/config.inc.php — strip the /tools/phpmyadmin prefix + const relativePath = step.path.replace( '/tools/phpmyadmin/', '' ); + const destFile = path.join( extractedPath, relativePath ); + await fs.ensureDir( path.dirname( destFile ) ); + await fs.writeFile( destFile, step.data ); + } + } } else { console.log( `[${ name }] Extracting files from zip ...` ); await extractZip( zipPath, extractedPath );