Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
419 changes: 419 additions & 0 deletions apps/cli/commands/site/convert.ts

Large diffs are not rendered by default.

47 changes: 42 additions & 5 deletions apps/cli/commands/site/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ import { installAiInstructionsToSite } from '@studio/common/lib/agent-skills';
import { extractFormValuesFromBlueprint } from '@studio/common/lib/blueprint-settings';
import { validateBlueprintData } from '@studio/common/lib/blueprint-validation';
import { SITE_EVENTS } from '@studio/common/lib/cli-events';
import {
DATABASE_ENGINE_MYSQL,
DATABASE_ENGINE_SQLITE,
type DatabaseEngine,
} from '@studio/common/lib/database-engine';
import { getDomainNameValidationError } from '@studio/common/lib/domains';
import {
arePathsEqual,
Expand Down Expand Up @@ -75,6 +80,7 @@ import {
import { updateServerFiles } from 'cli/lib/dependency-management/setup';
import { downloadWordPress } from 'cli/lib/dependency-management/wordpress';
import { copyLanguagePackToSite } from 'cli/lib/language-packs';
import { createMysqlSiteConfig } from 'cli/lib/mysql/mysql-site';
import { validateSupportedPhpVersion } from 'cli/lib/php-versions';
import { getPreferredSiteLanguage } from 'cli/lib/site-language';
import { generateSiteName } from 'cli/lib/site-name';
Expand All @@ -97,6 +103,7 @@ export type CreateCommandOptions = {
phpVersion: SupportedPHPVersion;
runtime: SiteRuntime;
fileAccess: SiteFileAccess;
databaseEngine?: DatabaseEngine;
customDomain?: string;
enableHttps: boolean;
blueprint?: {
Expand All @@ -116,13 +123,17 @@ export async function runCommand(
options: CreateCommandOptions
): Promise< void > {
const siteRuntime = options.runtime;
const databaseEngine = options.databaseEngine ?? DATABASE_ENGINE_SQLITE;
if ( ! isFileAccessAllowedForRuntime( siteRuntime, options.fileAccess ) ) {
throw new LoggerError(
__(
'File access "all-files" requires the native PHP runtime. The sandbox only has access to the site directory.'
)
);
}
if ( databaseEngine === DATABASE_ENGINE_MYSQL && siteRuntime !== SITE_RUNTIME_NATIVE_PHP ) {
throw new LoggerError( __( 'MySQL requires the native PHP runtime.' ) );
}
const phpVersion = validateSupportedPhpVersion( options.phpVersion );
const isOnlineStatus = await isOnline();

Expand Down Expand Up @@ -260,9 +271,11 @@ export async function runCommand(
logger.reportSuccess( __( 'WordPress files copied' ) );
}

logger.reportStart( LoggerAction.INSTALL_SQLITE, __( 'Setting up SQLite integration…' ) );
await keepSqliteIntegrationUpdated( sitePath );
logger.reportSuccess( __( 'SQLite integration configured' ) );
if ( databaseEngine === DATABASE_ENGINE_SQLITE ) {
logger.reportStart( LoggerAction.INSTALL_SQLITE, __( 'Setting up SQLite integration…' ) );
await keepSqliteIntegrationUpdated( sitePath );
logger.reportSuccess( __( 'SQLite integration configured' ) );
}

try {
const sharedConfig = await readSharedConfig();
Expand All @@ -282,6 +295,10 @@ export async function runCommand(

const siteName = options.name || path.basename( sitePath );
const siteId = options.siteId || crypto.randomUUID();
const mysqlConfig =
databaseEngine === DATABASE_ENGINE_MYSQL
? createMysqlSiteConfig( siteId, await portFinder.getOpenPort() )
: undefined;

// Determine admin credentials: CLI args > Blueprint > defaults
// External passwords need to be encoded; createPassword() already returns encoded
Expand Down Expand Up @@ -324,6 +341,8 @@ export async function runCommand(
phpVersion,
runtime: siteRuntime,
fileAccess: options.fileAccess,
databaseEngine: mysqlConfig ? DATABASE_ENGINE_MYSQL : undefined,
mysql: mysqlConfig,
running: false,
status: 'ready',
isWpAutoUpdating: options.wpVersion === DEFAULT_WORDPRESS_VERSION,
Expand Down Expand Up @@ -367,7 +386,9 @@ export async function runCommand(
} );
logger.reportSuccess( __( 'WordPress server started' ) );

stripWpConfigDbConstants( sitePath );
if ( databaseEngine === DATABASE_ENGINE_SQLITE ) {
stripWpConfigDbConstants( sitePath );
}

if ( processDesc.status === 'online' ) {
await updateSiteLatestCliPid( siteDetails.id, processDesc.pid );
Expand All @@ -386,6 +407,9 @@ export async function runCommand(
}
} catch ( error ) {
await removeSiteFromConfig( siteDetails.id );
if ( mysqlConfig ) {
await fs.promises.rm( mysqlConfig.dataDir, { recursive: true, force: true } );
}
if ( ! isWordPressDirResult ) {
await fs.promises.rm( sitePath, { recursive: true, force: true } );
}
Expand All @@ -410,9 +434,14 @@ export async function runCommand(
} );
logger.reportSuccess( __( 'Blueprint applied successfully' ) );

stripWpConfigDbConstants( sitePath );
if ( databaseEngine === DATABASE_ENGINE_SQLITE ) {
stripWpConfigDbConstants( sitePath );
}
} catch ( error ) {
await removeSiteFromConfig( siteDetails.id );
if ( mysqlConfig ) {
await fs.promises.rm( mysqlConfig.dataDir, { recursive: true, force: true } );
}
if ( ! isWordPressDirResult ) {
await fs.promises.rm( sitePath, { recursive: true, force: true } );
}
Expand Down Expand Up @@ -553,6 +582,12 @@ export const registerCommand = ( yargs: StudioArgv ) => {
choices: [ SITE_FILE_ACCESS_SITE_DIRECTORY, SITE_FILE_ACCESS_ALL_FILES ] as const,
default: SITE_FILE_ACCESS_SITE_DIRECTORY,
} )
.option( 'database-engine', {
type: 'string',
describe: __( 'Database engine for the site' ),
choices: [ DATABASE_ENGINE_SQLITE, DATABASE_ENGINE_MYSQL ] as const,
default: DATABASE_ENGINE_SQLITE,
} )
.option( 'domain', {
type: 'string',
describe: __( 'Custom domain (e.g., "mysite.local")' ),
Expand Down Expand Up @@ -612,6 +647,7 @@ export const registerCommand = ( yargs: StudioArgv ) => {
let adminEmail = argv.adminEmail;
const runtime = siteRuntimeFromMode( argv.runtime );
const fileAccess = argv.fileAccess;
const databaseEngine = argv.databaseEngine as DatabaseEngine;
if ( ! isFileAccessAllowedForRuntime( runtime, fileAccess ) ) {
logger.reportError(
new LoggerError(
Expand Down Expand Up @@ -789,6 +825,7 @@ export const registerCommand = ( yargs: StudioArgv ) => {
phpVersion: phpVersion ?? RecommendedPHPVersion,
runtime,
fileAccess,
databaseEngine,
customDomain,
enableHttps,
adminUsername,
Expand Down
15 changes: 9 additions & 6 deletions apps/cli/commands/site/start.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { updateManagedInstructionFiles } from '@studio/common/lib/agent-skills';
import { isMysqlSite } from '@studio/common/lib/database-engine';
import { SiteCommandLoggerAction as LoggerAction } from '@studio/common/logger-actions';
import { __, sprintf } from '@wordpress/i18n';
import { getSiteByFolder, updateSiteLatestCliPid } from 'cli/lib/cli-config/sites';
Expand Down Expand Up @@ -63,12 +64,14 @@ export async function runCommand(

await setupCustomDomain( site, logger );

logger.reportStart(
LoggerAction.INSTALL_SQLITE,
__( 'Setting up SQLite integration, if needed…' )
);
await keepSqliteIntegrationUpdated( sitePath );
logger.reportSuccess( __( 'SQLite integration configured as needed' ) );
if ( ! isMysqlSite( site ) ) {
logger.reportStart(
LoggerAction.INSTALL_SQLITE,
__( 'Setting up SQLite integration, if needed…' )
);
await keepSqliteIntegrationUpdated( sitePath );
logger.reportSuccess( __( 'SQLite integration configured as needed' ) );
}

try {
await updateManagedInstructionFiles( sitePath, getAiInstructionsPath() );
Expand Down
45 changes: 45 additions & 0 deletions apps/cli/commands/site/tests/create.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import fs from 'fs';
import path from 'path';
import { validateBlueprintData } from '@studio/common/lib/blueprint-validation';
import { DATABASE_ENGINE_MYSQL } from '@studio/common/lib/database-engine';
import {
isEmptyDir,
isWordPressDirectory,
Expand Down Expand Up @@ -54,6 +55,7 @@ vi.mock( '@studio/common/lib/port-finder', () => ( {
} ) );
vi.mock( '@studio/common/lib/passwords', () => ( {
createPassword: vi.fn().mockReturnValue( 'generated-password-123' ),
encodePassword: vi.fn( ( password: string ) => password ),
} ) );
vi.mock( '@studio/common/lib/blueprint-validation' );
vi.mock( 'cli/lib/cli-config/core', async () => {
Expand Down Expand Up @@ -325,6 +327,38 @@ describe( 'CLI: studio site create', () => {
);
} );

it( 'should persist MySQL configuration and skip SQLite integration for native PHP sites', async () => {
vi.mocked( portFinder.getOpenPort )
.mockResolvedValueOnce( mockPort )
.mockResolvedValueOnce( 8899 );

await runCommand( mockSitePath, {
...defaultTestOptions,
runtime: SITE_RUNTIME_NATIVE_PHP,
phpVersion: '8.4',
databaseEngine: DATABASE_ENGINE_MYSQL,
noStart: true,
} );

expect( keepSqliteIntegrationUpdated ).not.toHaveBeenCalled();
expect( saveCliConfig ).toHaveBeenCalledWith(
expect.objectContaining( {
sites: expect.arrayContaining( [
expect.objectContaining( {
runtime: SITE_RUNTIME_NATIVE_PHP,
databaseEngine: DATABASE_ENGINE_MYSQL,
mysql: expect.objectContaining( {
host: '127.0.0.1',
port: 8899,
serverVersion: '8.4.10',
} ),
} ),
] ),
} )
);
expect( startWordPressServer ).not.toHaveBeenCalled();
} );

it( 'should reject "all-files" file access for sandbox sites', async () => {
await expect(
runCommand( mockSitePath, {
Expand All @@ -336,6 +370,17 @@ describe( 'CLI: studio site create', () => {
expect( saveCliConfig ).not.toHaveBeenCalled();
} );

it( 'should reject MySQL for sandbox sites', async () => {
await expect(
runCommand( mockSitePath, {
...defaultTestOptions,
databaseEngine: DATABASE_ENGINE_MYSQL,
} )
).rejects.toThrow( 'MySQL requires the native PHP runtime.' );

expect( saveCliConfig ).not.toHaveBeenCalled();
} );

it( 'should create site with custom name', async () => {
await runCommand( mockSitePath, {
...defaultTestOptions,
Expand Down
3 changes: 3 additions & 0 deletions apps/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { registerCommand as registerMcpCommand } from 'cli/commands/mcp';
import { registerCommand as registerPullCommand } from 'cli/commands/pull';
import { registerCommand as registerPullReprintCommand } from 'cli/commands/pull-reprint';
import { registerCommand as registerPushCommand } from 'cli/commands/push';
import { registerCommand as registerSiteConvertCommand } from 'cli/commands/site/convert';
import { registerCommand as registerSiteCreateCommand } from 'cli/commands/site/create';
import { registerCommand as registerSiteDeleteCommand } from 'cli/commands/site/delete';
import { registerCommand as registerSiteListCommand } from 'cli/commands/site/list';
Expand Down Expand Up @@ -206,6 +207,7 @@ async function main() {
registerSiteStopCommand( studioArgv );
registerSiteDeleteCommand( studioArgv );
registerSiteStatusCommand( studioArgv );
registerSiteConvertCommand( studioArgv );

registerPushCommand( studioArgv );
registerPullCommand( studioArgv );
Expand Down Expand Up @@ -303,6 +305,7 @@ async function main() {
registerSiteStartCommand( sitesYargs );
registerSiteStopCommand( sitesYargs );
registerSiteDeleteCommand( sitesYargs );
registerSiteConvertCommand( sitesYargs );
registerSiteSetCommand( sitesYargs );

sitesYargs.version( false ).demandCommand( 1, __( 'You must provide a valid command' ) );
Expand Down
Loading
Loading