Skip to content
Closed
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
17 changes: 17 additions & 0 deletions apps/cli/ai/tests/wp-cli-description.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { getSiteDatabaseEngine } from '@studio/common/lib/site-database';
import { runWpCliTool } from '../tools/wp-cli';

vi.mock( '@studio/common/lib/site-database', async ( importActual ) => ( {
...( await importActual< typeof import('@studio/common/lib/site-database') >() ),
getSiteDatabaseEngine: vi.fn().mockReturnValue( 'sqlite' ),
} ) );

describe( 'WP-CLI database description', () => {
it( 'uses the default engine helper and preserves the SQLite guidance verbatim', () => {
expect( getSiteDatabaseEngine ).toHaveBeenCalledWith();
expect( runWpCliTool.description ).toContain(
'Studio sites use SQLite, not MySQL (the database file is `<site-path>/wp-content/database/.ht.sqlite`): `db query` and the other `db` subcommands do not work, so query WordPress data with object commands such as `post list`, `option get`, or `user list`. ' +
'Users can browse the database themselves with phpMyAdmin from the site overview in the Studio app while the site is running, or with `sqlite3 <site-path>/wp-content/database/.ht.sqlite` (pre-installed on macOS).'
);
} );
} );
15 changes: 13 additions & 2 deletions apps/cli/ai/tools/wp-cli.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { getSiteDatabaseEngine, getSiteDatabasePath } from '@studio/common/lib/site-database';
import { Type } from 'typebox';
import { connectToDaemon, disconnectFromDaemon } from 'cli/lib/daemon-client';
import { runWpCliCommand } from 'cli/lib/run-wp-cli-command';
Expand Down Expand Up @@ -114,14 +115,24 @@ function getUnsupportedWpCliOptionMessage( args: string[] ): string | null {
return `Unsupported WP-CLI option "${ unsupportedOption }": use ASCII hyphens, for example "--porcelain", not a typographic dash.`;
}

function getDatabaseDescription(): string {
if ( getSiteDatabaseEngine() !== 'sqlite' ) {
return '';
}
const databasePath = getSiteDatabasePath( '<site-path>' ).replace( /\\/g, '/' );
return (
`Studio sites use SQLite, not MySQL (the database file is \`${ databasePath }\`): \`db query\` and the other \`db\` subcommands do not work, so query WordPress data with object commands such as \`post list\`, \`option get\`, or \`user list\`. ` +
`Users can browse the database themselves with phpMyAdmin from the site overview in the Studio app while the site is running, or with \`sqlite3 ${ databasePath }\` (pre-installed on macOS).`
);
}

export const runWpCliTool = defineTool(
'wp_cli',
'Runs a WP-CLI command on a specific WordPress site. The site must be running (site_start if needed). ' +
'Examples: "plugin install woocommerce --activate", "option get blogname", "user list". ' +
'The command takes literal arguments, not shell syntax — never use shell substitution or shell syntax such as `$(cat file)`, backticks, pipes, redirection, or environment variables to provide post content. ' +
'For large post content, write the validated markup to a scratch file inside the site directory and pass its path to `post create <file>` (or `post update <id> <file>`) — this avoids the OS command-length limit. For smaller content you may instead pass it inline with `--post_content=...` as the final argument. ' +
'Studio sites use SQLite, not MySQL (the database file is `<site-path>/wp-content/database/.ht.sqlite`): `db query` and the other `db` subcommands do not work, so query WordPress data with object commands such as `post list`, `option get`, or `user list`. ' +
'Users can browse the database themselves with phpMyAdmin from the site overview in the Studio app while the site is running, or with `sqlite3 <site-path>/wp-content/database/.ht.sqlite` (pre-installed on macOS).',
getDatabaseDescription(),
{
nameOrPath: Type.String( { description: 'The site name or file system path to the site' } ),
command: Type.String( {
Expand Down
3 changes: 2 additions & 1 deletion apps/cli/commands/config/set.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
validateAdminEmail,
validateAdminUsername,
} from '@studio/common/lib/passwords';
import { getSiteDatabasePath } from '@studio/common/lib/site-database';
import {
getSiteFileAccess,
siteFileAccessSchema,
Expand Down Expand Up @@ -81,7 +82,7 @@ async function isUnprovisionedSite( site: SiteData ): Promise< boolean > {
}
const [ hasConfig, hasDatabase ] = await Promise.all( [
pathExists( path.join( site.path, 'wp-config.php' ) ),
pathExists( path.join( site.path, 'wp-content', 'database', '.ht.sqlite' ) ),
pathExists( getSiteDatabasePath( site.path ) ),
] );
return ! hasConfig && ! hasDatabase;
}
Expand Down
4 changes: 2 additions & 2 deletions apps/cli/commands/export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import { connectToDaemon, disconnectFromDaemon } from 'cli/lib/daemon-client';
import { ImportExportEventEmitter } from 'cli/lib/import-export/events';
import { getExporter } from 'cli/lib/import-export/export/export-manager';
import { ExportOptions } from 'cli/lib/import-export/export/types';
import { ensureSiteDatabaseReady } from 'cli/lib/site-database';
import { withSiteOperation } from 'cli/lib/site-operations';
import { keepSqliteIntegrationUpdated } from 'cli/lib/sqlite-integration';
import { getTracksOrigin, recordTracksEvent, TRACKS_EVENTS } from 'cli/lib/tracks';
import { classifyExportFailure, untildify } from 'cli/lib/utils';
import { Logger, LoggerError } from 'cli/logger';
Expand Down Expand Up @@ -169,7 +169,7 @@ async function exportSite(
LoggerAction.INSTALL_SQLITE,
__( 'Setting up SQLite integration, if needed…' )
);
await keepSqliteIntegrationUpdated( siteFolder );
await ensureSiteDatabaseReady( siteFolder );
logger.reportSuccess( __( 'SQLite integration configured as needed' ) );

const includes: ExportOptions[ 'includes' ] = { database: true, wpContent: true };
Expand Down
4 changes: 2 additions & 2 deletions apps/cli/commands/import.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ import { connectToDaemon, disconnectFromDaemon, emitCliEvent } from 'cli/lib/dae
import { ImportExportEventEmitter } from 'cli/lib/import-export/events';
import { DEFAULT_IMPORTER_OPTIONS, getImporter } from 'cli/lib/import-export/import/import-manager';
import { getBackupFileType } from 'cli/lib/import-export/utils';
import { ensureSiteDatabaseReady } from 'cli/lib/site-database';
import { withSiteOperation } from 'cli/lib/site-operations';
import { keepSqliteIntegrationUpdated } from 'cli/lib/sqlite-integration';
import { reportSyncActivity } from 'cli/lib/sync-activity';
import { getTracksOrigin, recordTracksEvent, TRACKS_EVENTS } from 'cli/lib/tracks';
import { classifyImportFailure, untildify } from 'cli/lib/utils';
Expand Down Expand Up @@ -366,7 +366,7 @@ async function importBackup(
LoggerAction.INSTALL_SQLITE,
__( 'Setting up SQLite integration, if needed…' )
);
await keepSqliteIntegrationUpdated( siteFolder );
await ensureSiteDatabaseReady( siteFolder );
logger.reportSuccess( __( 'SQLite integration configured as needed' ) );

logger.reportStart( LoggerAction.START_SITE, __( 'Starting WordPress server…' ) );
Expand Down
18 changes: 9 additions & 9 deletions apps/cli/commands/pull-reprint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import os from 'os';
import path from 'path';
import { encodePassword } from '@studio/common/lib/passwords';
import { readAuthToken, type StoredAuthToken } from '@studio/common/lib/shared-config';
import { getSiteDatabasePath } from '@studio/common/lib/site-database';
import { PullReprintCommandLoggerAction as LoggerAction } from '@studio/common/logger-actions';
import { __, sprintf } from '@wordpress/i18n';
import chalk from 'chalk';
Expand Down Expand Up @@ -608,9 +609,9 @@ async function applySelection( params: {
? path.join( session.rawDirectory, ...contentDir.split( '/' ).filter( Boolean ) )
: path.join( session.rawDirectory, 'wp-content' );
const candidates = [
path.join( session.sitePath, 'wp-content', 'database', '.ht.sqlite' ),
path.join( rawContentPath, 'database', '.ht.sqlite' ),
path.join( session.rawDirectory, 'wp-content', 'database', '.ht.sqlite' ),
getSiteDatabasePath( session.sitePath ),
path.join( rawContentPath, path.relative( 'wp-content', getSiteDatabasePath( '' ) ) ),
getSiteDatabasePath( session.rawDirectory ),
];
if ( ! candidates.some( ( candidate ) => fs.existsSync( candidate ) ) ) {
throw new LoggerError(
Expand Down Expand Up @@ -903,21 +904,20 @@ export async function runFullPull(
importedSqlitePath = path.join(
metadata.rawDirectory,
...contentDir.split( '/' ).filter( Boolean ),
'database',
'.ht.sqlite'
path.relative( 'wp-content', getSiteDatabasePath( '' ) )
);
} else if ( selection.skipDatabase ) {
importedSqlitePath = path.join( metadata.sitePath, 'wp-content', 'database', '.ht.sqlite' );
importedSqlitePath = getSiteDatabasePath( metadata.sitePath );
} else {
importedSqlitePath = path.join( metadata.rawDirectory, 'wp-content', 'database', '.ht.sqlite' );
importedSqlitePath = getSiteDatabasePath( metadata.rawDirectory );
}

let sqlitePath = importedSqlitePath;
if ( selection.skipDatabase ) {
const existingSqlitePath = [
importedSqlitePath,
path.join( metadata.sitePath, 'wp-content', 'database', '.ht.sqlite' ),
path.join( metadata.rawDirectory, 'wp-content', 'database', '.ht.sqlite' ),
getSiteDatabasePath( metadata.sitePath ),
getSiteDatabasePath( metadata.rawDirectory ),
].find( ( candidate ) => fs.existsSync( candidate ) );
if ( existingSqlitePath ) {
sqlitePath = existingSqlitePath;
Expand Down
4 changes: 2 additions & 2 deletions apps/cli/commands/push.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ import { __, sprintf } from '@wordpress/i18n';
import { getSiteByFolder } from 'cli/lib/cli-config/sites';
import { getExporter } from 'cli/lib/import-export/export/export-manager';
import { ExportOptions } from 'cli/lib/import-export/export/types';
import { ensureSiteDatabaseReady } from 'cli/lib/site-database';
import { withSiteOperation } from 'cli/lib/site-operations';
import { keepSqliteIntegrationUpdated } from 'cli/lib/sqlite-integration';
import { exitOnCancel, reportSyncActivity } from 'cli/lib/sync-activity';
import {
fetchSyncableSites,
Expand Down Expand Up @@ -134,7 +134,7 @@ async function runPush(
LoggerAction.INSTALL_SQLITE,
__( 'Setting up SQLite integration, if needed…' )
);
await keepSqliteIntegrationUpdated( siteFolder );
await ensureSiteDatabaseReady( siteFolder );
logger.reportSuccess( __( 'SQLite integration configured as needed' ) );

logger.reportStart( LoggerAction.FETCH_REMOTE_SITES, __( 'Fetching WordPress.com sites…' ) );
Expand Down
4 changes: 2 additions & 2 deletions apps/cli/commands/site/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,11 @@ import { updateServerFiles } from 'cli/lib/dependency-management/setup';
import { downloadWordPress } from 'cli/lib/dependency-management/wordpress';
import { copyLanguagePackToSite } from 'cli/lib/language-packs';
import { validateSupportedPhpVersion } from 'cli/lib/php-versions';
import { ensureSiteDatabaseReady } from 'cli/lib/site-database';
import { getPreferredSiteLanguage } from 'cli/lib/site-language';
import { generateSiteName } from 'cli/lib/site-name';
import { getDefaultSitePath } from 'cli/lib/site-paths';
import { logSiteDetails, openSiteInBrowser, setupCustomDomain } from 'cli/lib/site-utils';
import { keepSqliteIntegrationUpdated } from 'cli/lib/sqlite-integration';
import {
completeSourceImport,
prepareSourceImport,
Expand Down Expand Up @@ -290,7 +290,7 @@ export async function runCommand(
}

logger.reportStart( LoggerAction.INSTALL_SQLITE, __( 'Setting up SQLite integration…' ) );
await keepSqliteIntegrationUpdated( sitePath );
await ensureSiteDatabaseReady( sitePath );
logger.reportSuccess( __( 'SQLite integration configured' ) );

try {
Expand Down
4 changes: 2 additions & 2 deletions apps/cli/commands/site/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import { __, sprintf } from '@wordpress/i18n';
import { getSiteByFolder, updateSiteLatestCliPid } from 'cli/lib/cli-config/sites';
import { connectToDaemon, disconnectFromDaemon } from 'cli/lib/daemon-client';
import { getAiInstructionsPath } from 'cli/lib/dependency-management/paths';
import { ensureSiteDatabaseReady } from 'cli/lib/site-database';
import { withSiteOperation } from 'cli/lib/site-operations';
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';
Expand Down Expand Up @@ -81,7 +81,7 @@ async function startSite(
LoggerAction.INSTALL_SQLITE,
__( 'Setting up SQLite integration, if needed…' )
);
await keepSqliteIntegrationUpdated( sitePath );
await ensureSiteDatabaseReady( sitePath );
logger.reportSuccess( __( 'SQLite integration configured as needed' ) );

try {
Expand Down
17 changes: 6 additions & 11 deletions apps/cli/commands/site/tests/create.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,9 @@ import { updateServerFiles } from 'cli/lib/dependency-management/setup';
import { downloadWordPress } from 'cli/lib/dependency-management/wordpress';
import { copyLanguagePackToSite } from 'cli/lib/language-packs';
import { runWpCliCommand } from 'cli/lib/run-wp-cli-command';
import { isSqliteIntegrationAvailable, ensureSiteDatabaseReady } from 'cli/lib/site-database';
import { getPreferredSiteLanguage } from 'cli/lib/site-language';
import { logSiteDetails, openSiteInBrowser, setupCustomDomain } from 'cli/lib/site-utils';
import {
isSqliteIntegrationAvailable,
keepSqliteIntegrationUpdated,
} from 'cli/lib/sqlite-integration';
import { buildCreateFromSourceBlueprint } from 'cli/lib/static-site-import/source';
import { recordTracksEvent, TRACKS_EVENTS } from 'cli/lib/tracks';
import { ProcessDescription } from 'cli/lib/types/process-manager-ipc';
Expand Down Expand Up @@ -96,7 +93,7 @@ vi.mock( import( '@studio/common/lib/well-known-paths' ), async ( importOriginal
vi.mock( 'cli/lib/site-language' );
vi.mock( 'cli/lib/site-utils' );
vi.mock( '@studio/common/lib/agent-skills' );
vi.mock( 'cli/lib/sqlite-integration' );
vi.mock( 'cli/lib/site-database' );
vi.mock( 'cli/lib/run-wp-cli-command' );
vi.mock( 'cli/lib/wordpress-server-manager' );
const { staticSiteImporterFixture } = vi.hoisted( () => {
Expand Down Expand Up @@ -254,7 +251,7 @@ describe( 'CLI: studio create', () => {
vi.mocked( saveCliConfig ).mockResolvedValue( undefined );
vi.mocked( lockCliConfig ).mockResolvedValue( undefined );
vi.mocked( unlockCliConfig ).mockResolvedValue( undefined );
vi.mocked( keepSqliteIntegrationUpdated ).mockResolvedValue( undefined );
vi.mocked( ensureSiteDatabaseReady ).mockResolvedValue( undefined );
vi.mocked( isSqliteIntegrationAvailable ).mockResolvedValue( true );
vi.mocked( connectToDaemon ).mockResolvedValue( undefined );
vi.mocked( disconnectFromDaemon ).mockResolvedValue( undefined );
Expand Down Expand Up @@ -449,7 +446,7 @@ describe( 'CLI: studio create', () => {
} );

it( 'should error if SQLite integration is not available', async () => {
vi.mocked( keepSqliteIntegrationUpdated ).mockRejectedValue(
vi.mocked( ensureSiteDatabaseReady ).mockRejectedValue(
new Error( 'SQLite integration files not found. Please ensure Studio is installed.' )
);

Expand Down Expand Up @@ -1232,7 +1229,7 @@ describe( 'CLI: studio create', () => {
await runCommand( mockSitePath, { ...defaultTestOptions } );

expect( fsMkdirSyncSpy ).toHaveBeenCalledWith( mockSitePath, { recursive: true } );
expect( keepSqliteIntegrationUpdated ).toHaveBeenCalledWith( mockSitePath );
expect( ensureSiteDatabaseReady ).toHaveBeenCalledWith( mockSitePath );
expect( loggerReportSuccessSpy ).toHaveBeenCalledWith( 'SQLite integration configured' );
expect( portFinder.getOpenPort ).toHaveBeenCalled();
expect( lockCliConfig ).toHaveBeenCalled();
Expand Down Expand Up @@ -1815,9 +1812,7 @@ describe( 'CLI: studio create', () => {
} );

it( 'should handle SQLite setup failure', async () => {
vi.mocked( keepSqliteIntegrationUpdated ).mockRejectedValue(
new Error( 'SQLite setup failed' )
);
vi.mocked( ensureSiteDatabaseReady ).mockRejectedValue( new Error( 'SQLite setup failed' ) );

await expect( runCommand( mockSitePath, { ...defaultTestOptions } ) ).rejects.toThrow();

Expand Down
14 changes: 6 additions & 8 deletions apps/cli/commands/site/tests/start.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { vi } from 'vitest';
import { SiteData } from 'cli/lib/cli-config/core';
import { getSiteByFolder, updateSiteLatestCliPid } from 'cli/lib/cli-config/sites';
import { connectToDaemon, disconnectFromDaemon } from 'cli/lib/daemon-client';
import { ensureSiteDatabaseReady } from 'cli/lib/site-database';
import { logSiteDetails, openSiteInBrowser, setupCustomDomain } from 'cli/lib/site-utils';
import { keepSqliteIntegrationUpdated } from 'cli/lib/sqlite-integration';
import { ProcessDescription } from 'cli/lib/types/process-manager-ipc';
import { isServerRunning, startWordPressServer } from 'cli/lib/wordpress-server-manager';
import { Logger } from 'cli/logger';
Expand All @@ -24,7 +24,7 @@ vi.mock( 'cli/lib/site-operations', async ( importOriginal ) => ( {
} ) );
vi.mock( 'cli/lib/site-utils' );
vi.mock( 'cli/lib/wordpress-server-manager' );
vi.mock( 'cli/lib/sqlite-integration' );
vi.mock( 'cli/lib/site-database' );

describe( 'CLI: studio site start', () => {
// Simple test data
Expand Down Expand Up @@ -59,7 +59,7 @@ describe( 'CLI: studio site start', () => {
vi.mocked( disconnectFromDaemon ).mockResolvedValue( undefined );
vi.mocked( isServerRunning ).mockResolvedValue( undefined );
vi.mocked( setupCustomDomain ).mockResolvedValue( undefined );
vi.mocked( keepSqliteIntegrationUpdated ).mockResolvedValue( undefined );
vi.mocked( ensureSiteDatabaseReady ).mockResolvedValue( undefined );
vi.mocked( startWordPressServer ).mockResolvedValue( testProcessDescription );
vi.mocked( updateSiteLatestCliPid ).mockResolvedValue( undefined );
vi.mocked( logSiteDetails ).mockImplementation( () => {} );
Expand Down Expand Up @@ -126,9 +126,7 @@ describe( 'CLI: studio site start', () => {
} );

it( 'should throw when SQLite integration setup fails', async () => {
vi.mocked( keepSqliteIntegrationUpdated ).mockRejectedValue(
new Error( 'SQLite setup failed' )
);
vi.mocked( ensureSiteDatabaseReady ).mockRejectedValue( new Error( 'SQLite setup failed' ) );

await expect( runCommand( '/test/site' ) ).rejects.toThrow( 'SQLite setup failed' );
expect( disconnectFromDaemon ).toHaveBeenCalled();
Expand Down Expand Up @@ -158,7 +156,7 @@ describe( 'CLI: studio site start', () => {
expect( connectToDaemon ).toHaveBeenCalled();
expect( isServerRunning ).toHaveBeenCalledWith( testSite.id );
expect( setupCustomDomain ).toHaveBeenCalledWith( testSite, expect.any( Logger ) );
expect( keepSqliteIntegrationUpdated ).toHaveBeenCalledWith( '/test/site' );
expect( ensureSiteDatabaseReady ).toHaveBeenCalledWith( '/test/site' );
expect( startWordPressServer ).toHaveBeenCalledWith( testSite, expect.any( Logger ) );
expect( logSiteDetails ).toHaveBeenCalledWith( testSite );
expect( openSiteInBrowser ).toHaveBeenCalledWith( testSite );
Expand Down Expand Up @@ -197,7 +195,7 @@ describe( 'CLI: studio site start', () => {
it( 'should update SQLite integration', async () => {
await runCommand( '/test/site' );

expect( keepSqliteIntegrationUpdated ).toHaveBeenCalledWith( '/test/site' );
expect( ensureSiteDatabaseReady ).toHaveBeenCalledWith( '/test/site' );
} );

it( 'should skip browser when skipBrowser is true', async () => {
Expand Down
Loading
Loading