Skip to content
7 changes: 2 additions & 5 deletions cli/commands/site/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,7 @@ import { SiteCommandLoggerAction as LoggerAction } from 'common/logger-actions';
import { lockAppdata, readAppdata, saveAppdata, SiteData, unlockAppdata } from 'cli/lib/appdata';
import { connect, disconnect } from 'cli/lib/pm2-manager';
import { logSiteDetails, openSiteInBrowser, setupCustomDomain } from 'cli/lib/site-utils';
import {
isSqliteIntegrationAvailable,
keepSqliteIntegrationUpdated,
} from 'cli/lib/sqlite-integration';
import { installSqliteIntegration, isSqliteIntegrationAvailable } from 'cli/lib/sqlite-integration';
import { runBlueprint, startWordPressServer } from 'cli/lib/wordpress-server-manager';
import { Logger, LoggerError } from 'cli/logger';
import { StudioArgv } from 'cli/types';
Expand Down Expand Up @@ -154,7 +151,7 @@ export async function runCommand(
);
}
logger.reportStart( LoggerAction.INSTALL_SQLITE, __( 'Setting up SQLite integration...' ) );
await keepSqliteIntegrationUpdated( sitePath );
await installSqliteIntegration( sitePath );
logger.reportSuccess( __( 'SQLite integration configured' ) );

logger.reportStart( LoggerAction.ASSIGN_PORT, __( 'Assigning port...' ) );
Expand Down
12 changes: 10 additions & 2 deletions cli/commands/site/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,17 @@ import { SiteCommandLoggerAction as LoggerAction } from 'common/logger-actions';
import { getSiteByFolder, updateSiteLatestCliPid } from 'cli/lib/appdata';
import { connect, disconnect } from 'cli/lib/pm2-manager';
import { logSiteDetails, openSiteInBrowser, setupCustomDomain } from 'cli/lib/site-utils';
import { keepSqliteIntegrationUpdated } from 'cli/lib/sqlite-integration';
import { isServerRunning, startWordPressServer } from 'cli/lib/wordpress-server-manager';
import { Logger, LoggerError } from 'cli/logger';
import { StudioArgv } from 'cli/types';

export async function runCommand( siteFolder: string, skipBrowser = false ): Promise< void > {
export async function runCommand( sitePath: string, skipBrowser = false ): Promise< void > {
const logger = new Logger< LoggerAction >();

try {
logger.reportStart( LoggerAction.LOAD_SITES, __( 'Loading site…' ) );
const site = await getSiteByFolder( siteFolder, false );
const site = await getSiteByFolder( sitePath, false );
logger.reportSuccess( __( 'Site loaded' ) );

logger.reportStart( LoggerAction.START_DAEMON, __( 'Starting process daemon...' ) );
Expand All @@ -34,6 +35,13 @@ export async function runCommand( siteFolder: string, skipBrowser = false ): Pro

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' ) );

logger.reportStart( LoggerAction.START_SITE, __( 'Starting WordPress site...' ) );
try {
const processDesc = await startWordPressServer( site );
Expand Down
11 changes: 4 additions & 7 deletions cli/commands/site/tests/create.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,7 @@ import { portFinder } from 'common/lib/port-finder';
import { lockAppdata, readAppdata, saveAppdata, unlockAppdata, SiteData } from 'cli/lib/appdata';
import { connect, disconnect } from 'cli/lib/pm2-manager';
import { logSiteDetails, openSiteInBrowser, setupCustomDomain } from 'cli/lib/site-utils';
import {
isSqliteIntegrationAvailable,
keepSqliteIntegrationUpdated,
} from 'cli/lib/sqlite-integration';
import { isSqliteIntegrationAvailable, installSqliteIntegration } from 'cli/lib/sqlite-integration';
import { runBlueprint, startWordPressServer } from 'cli/lib/wordpress-server-manager';
import { Logger, LoggerError } from 'cli/logger';

Expand Down Expand Up @@ -110,7 +107,7 @@ describe( 'Site Create Command', () => {
( lockAppdata as jest.Mock ).mockResolvedValue( undefined );
( unlockAppdata as jest.Mock ).mockResolvedValue( undefined );
( isSqliteIntegrationAvailable as jest.Mock ).mockResolvedValue( true );
( keepSqliteIntegrationUpdated as jest.Mock ).mockResolvedValue( undefined );
( installSqliteIntegration as jest.Mock ).mockResolvedValue( undefined );
( connect as jest.Mock ).mockResolvedValue( undefined );
( disconnect as jest.Mock ).mockReturnValue( undefined );
( setupCustomDomain as jest.Mock ).mockResolvedValue( undefined );
Expand Down Expand Up @@ -323,7 +320,7 @@ describe( 'Site Create Command', () => {
expect( mockLogger.reportSuccess ).toHaveBeenCalledWith( 'Site configuration validated' );
expect( fsMkdirSyncSpy ).toHaveBeenCalledWith( mockSitePath, { recursive: true } );
expect( isSqliteIntegrationAvailable ).toHaveBeenCalled();
expect( keepSqliteIntegrationUpdated ).toHaveBeenCalledWith( mockSitePath );
expect( installSqliteIntegration ).toHaveBeenCalledWith( mockSitePath );
expect( portFinder.getOpenPort ).toHaveBeenCalled();
expect( lockAppdata ).toHaveBeenCalled();
expect( saveAppdata ).toHaveBeenCalled();
Expand Down Expand Up @@ -694,7 +691,7 @@ describe( 'Site Create Command', () => {
} );

it( 'should handle SQLite setup failure', async () => {
( keepSqliteIntegrationUpdated as jest.Mock ).mockRejectedValue(
( installSqliteIntegration as jest.Mock ).mockRejectedValue(
new Error( 'SQLite setup failed' )
);

Expand Down
4 changes: 4 additions & 0 deletions cli/commands/site/tests/start.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { openBrowser } from 'cli/lib/browser';
import { generateSiteCertificate } from 'cli/lib/certificate-manager';
import { addDomainToHosts } from 'cli/lib/hosts-file';
import { connect, disconnect, isProxyProcessRunning, startProxyProcess } from 'cli/lib/pm2-manager';
import { keepSqliteIntegrationUpdated } from 'cli/lib/sqlite-integration';
import { isServerRunning, startWordPressServer } from 'cli/lib/wordpress-server-manager';
import { Logger, LoggerError } from 'cli/logger';

Expand All @@ -19,6 +20,8 @@ jest.mock( 'cli/lib/hosts-file' );
jest.mock( 'cli/lib/pm2-manager' );
jest.mock( 'cli/lib/wordpress-server-manager' );
jest.mock( 'cli/logger' );
jest.mock( 'cli/lib/sqlite-integration' );
jest.mock( 'common/lib/fs-utils' );

describe( 'Site Start Command', () => {
const mockSiteFolder = '/test/site/path';
Expand Down Expand Up @@ -102,6 +105,7 @@ describe( 'Site Start Command', () => {
( openBrowser as jest.Mock ).mockResolvedValue( undefined );
( generateSiteCertificate as jest.Mock ).mockResolvedValue( undefined );
( addDomainToHosts as jest.Mock ).mockResolvedValue( undefined );
( keepSqliteIntegrationUpdated as jest.Mock ).mockResolvedValue( undefined );
} );

afterEach( () => {
Expand Down
132 changes: 27 additions & 105 deletions cli/lib/sqlite-integration.ts
Original file line number Diff line number Diff line change
@@ -1,121 +1,43 @@
/**
* SQLite Integration for Studio CLI
*
* This module provides functions to set up SQLite integration for WordPress sites.
* It uses the SQLite plugin files from the Studio Desktop app's server-files directory.
*/
import fs from 'fs';
import os from 'os';
import path from 'path';
import { pathExists } from 'common/lib/fs-utils';
import { SqliteIntegrationProvider } from 'common/lib/sqlite-integration';

const SQLITE_FILENAME = 'sqlite-database-integration';

/**
* Get the path to Studio's server-files directory
*/
function getServerFilesPath(): string {
return path.join( os.homedir(), 'Library', 'Application Support', 'Studio', 'server-files' );
}

/**
* Get the path to the SQLite integration plugin in Studio's server-files
*/
function getSqlitePluginPath(): string {
return path.join( getServerFilesPath(), SQLITE_FILENAME );
}

/**
* Check if Studio's SQLite integration files are available
*/
export async function isSqliteIntegrationAvailable(): Promise< boolean > {
const sqlitePath = getSqlitePluginPath();
const dbCopyPath = path.join( sqlitePath, 'db.copy' );
return ( await pathExists( sqlitePath ) ) && ( await pathExists( dbCopyPath ) );
}

/**
* Check if a site needs SQLite integration setup
* Returns true if the site has db.php (indicating SQLite usage) or no wp-config.php
*/
export async function needsSqliteSetup( sitePath: string ): Promise< boolean > {
const hasDbPhp = await pathExists( path.join( sitePath, 'wp-content', 'db.php' ) );
const hasWpConfig = await pathExists( path.join( sitePath, 'wp-config.php' ) );
return hasDbPhp || ! hasWpConfig;
}

/**
* Install SQLite integration in a WordPress site
* This copies the necessary files from Studio's server-files to the site's wp-content directory
*/
export async function installSqliteIntegration( sitePath: string ): Promise< void > {
const sqliteSourcePath = getSqlitePluginPath();

if ( ! ( await pathExists( sqliteSourcePath ) ) ) {
throw new Error(
'SQLite integration files not found. Please ensure Studio Desktop is installed.'
);
class CliSqliteProvider extends SqliteIntegrationProvider {
getServerFilesPath(): string {
if ( process.platform === 'darwin' ) {
return path.join( os.homedir(), 'Library', 'Application Support', 'Studio', 'server-files' );
}
if ( process.platform === 'win32' ) {
if ( process.env.APPDATA ) {
return path.join( process.env.APPDATA, 'Studio', 'server-files' );
} else {
throw new Error( 'APPDATA environment variable is not set' );
}
}
throw new Error( 'Unsupported platform' );
}

const wpContentPath = path.join( sitePath, 'wp-content' );
const databasePath = path.join( wpContentPath, 'database' );

// Create database directory
fs.mkdirSync( databasePath, { recursive: true } );

// Copy db.php and update the path
const dbPhpPath = path.join( wpContentPath, 'db.php' );
const dbCopySource = path.join( sqliteSourcePath, 'db.copy' );

if ( ! ( await pathExists( dbCopySource ) ) ) {
throw new Error( 'SQLite db.copy file not found in Studio server-files.' );
getSqliteFilename(): string {
return SQLITE_FILENAME;
}

const dbCopyContent = fs.readFileSync( dbCopySource, 'utf8' );
const updatedContent = dbCopyContent.replace(
"'{SQLITE_IMPLEMENTATION_FOLDER_PATH}'",
`realpath( __DIR__ . '/mu-plugins/${ SQLITE_FILENAME }' )`
);
fs.writeFileSync( dbPhpPath, updatedContent );

// Copy SQLite mu-plugin
const muPluginsPath = path.join( wpContentPath, 'mu-plugins' );
fs.mkdirSync( muPluginsPath, { recursive: true } );

const sqliteDestPath = path.join( muPluginsPath, SQLITE_FILENAME );
copyDirectoryRecursive( sqliteSourcePath, sqliteDestPath );
}

/**
* Recursively copy a directory
*/
function copyDirectoryRecursive( source: string, destination: string ): void {
if ( ! fs.existsSync( source ) ) {
return;
}

fs.mkdirSync( destination, { recursive: true } );
const provider = new CliSqliteProvider();

const entries = fs.readdirSync( source, { withFileTypes: true } );
export async function isSqliteIntegrationAvailable() {
return provider.isSqliteIntegrationAvailable();
}

for ( const entry of entries ) {
const sourcePath = path.join( source, entry.name );
const destPath = path.join( destination, entry.name );
export async function needsSqliteSetup( sitePath: string ) {
return provider.needsSqliteSetup( sitePath );
}

if ( entry.isDirectory() ) {
copyDirectoryRecursive( sourcePath, destPath );
} else {
fs.copyFileSync( sourcePath, destPath );
}
}
export async function installSqliteIntegration( sitePath: string ) {
return provider.installSqliteIntegration( sitePath );
}

/**
* Set up SQLite integration for a site if needed
* This is the main entry point that checks conditions and installs SQLite
*/
export async function keepSqliteIntegrationUpdated( sitePath: string ): Promise< void > {
if ( await needsSqliteSetup( sitePath ) ) {
await installSqliteIntegration( sitePath );
}
export async function keepSqliteIntegrationUpdated( sitePath: string ) {
return provider.keepSqliteIntegrationUpdated( sitePath );
}
69 changes: 69 additions & 0 deletions common/lib/sqlite-integration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import path from 'path';
import fs from 'fs-extra';

// Abstract base class for SQLite integration across different contexts
export abstract class SqliteIntegrationProvider {
abstract getServerFilesPath(): string;
abstract getSqliteFilename(): string;

protected getSqlitePluginSourcePath(): string {
return path.join( this.getServerFilesPath(), this.getSqliteFilename() );
}

async isSqliteIntegrationAvailable(): Promise< boolean > {
const sqliteSourcePath = this.getSqlitePluginSourcePath();
const dbCopyPath = path.join( sqliteSourcePath, 'db.copy' );
return ( await fs.pathExists( sqliteSourcePath ) ) && ( await fs.pathExists( dbCopyPath ) );
}

// Returns true if site has db.php or no wp-config.php
async needsSqliteSetup( sitePath: string ): Promise< boolean > {
const hasDbPhp = await fs.pathExists( path.join( sitePath, 'wp-content', 'db.php' ) );
const hasWpConfig = await fs.pathExists( path.join( sitePath, 'wp-config.php' ) );
return hasDbPhp || ! hasWpConfig;
}

async getSqliteVersionFromInstallation( sqliteMuPluginPath: string ): Promise< string > {
try {
const versionFileContent = await fs.readFile(
path.join( sqliteMuPluginPath, 'load.php' ),
'utf8'
);
const matches = versionFileContent.match( /\s\*\sVersion:\s*([0-9a-zA-Z.-]+)/ );
return matches?.[ 1 ] || '';
} catch ( err ) {
return '';
}
}

async installSqliteIntegration( sitePath: string ): Promise< void > {
if ( ! ( await this.isSqliteIntegrationAvailable() ) ) {
throw new Error(
'SQLite integration files not found. Please ensure Studio Desktop is installed.'
);
}

const wpContentPath = path.join( sitePath, 'wp-content' );
const databasePath = path.join( wpContentPath, 'database' );

await fs.mkdir( databasePath, { recursive: true } );

const sqliteSourcePath = this.getSqlitePluginSourcePath();
const dbCopyContent = await fs.readFile( path.join( sqliteSourcePath, 'db.copy' ), 'utf8' );
const sqliteFilename = this.getSqliteFilename();
const updatedContent = dbCopyContent.replace(
"'{SQLITE_IMPLEMENTATION_FOLDER_PATH}'",
`realpath( __DIR__ . '/mu-plugins/${ sqliteFilename }' )`
);
await fs.writeFile( path.join( wpContentPath, 'db.php' ), updatedContent );

const sqliteDestPath = path.join( wpContentPath, 'mu-plugins', sqliteFilename );
await fs.copy( sqliteSourcePath, sqliteDestPath );
}

async keepSqliteIntegrationUpdated( sitePath: string ): Promise< void > {
if ( await this.needsSqliteSetup( sitePath ) ) {
await this.installSqliteIntegration( sitePath );
}
}
}
Loading