Skip to content
Merged
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
6 changes: 6 additions & 0 deletions apps/cli/ai/system-prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ IMPORTANT: Before doing ANY work, you MUST first check the site's plan by callin
- \`body\`: Optional request body for POST/PUT
- \`apiNamespace\`: Defaults to \`"wp/v2"\`. Set to \`""\` (empty string) for WP.com REST API v1.1, or \`"wpcom/v2"\` for WP.com v2 endpoints.
- **take_screenshot**: Take a full-page screenshot of a URL (supports desktop and mobile viewports)
- **site_create**: Create a new local WordPress site (use this to create a local site before pulling remote content into it)
- **site_pull**: Pull the remote WordPress.com site to a local site. Create a local site first with site_create, then pull into it. Specify sync options (all, sqls, uploads, plugins, themes, contents).

## API Namespace Guide

Expand Down Expand Up @@ -134,6 +136,10 @@ Then continue with:
- validate_blocks: Validate block content for correctness on a running site (runs each block through its save() function in a real browser). Requires a site name or path. Call after every file write/edit that contains block content.
- take_screenshot: Take a full-page screenshot of a URL (supports desktop and mobile viewports). Use this to visually check the site after building it.
- audit_performance: Measure frontend performance metrics (TTFB, FCP, LCP, CLS, page weight, DOM size, JS/CSS/image/font asset breakdown) for a running site. Use this to identify performance bottlenecks and guide optimization.
- site_push: Push a local site to a WordPress.com site. Requires authentication (studio auth login). Specify the remote site URL or ID and sync options (all, sqls, uploads, plugins, themes, contents).
- site_pull: Pull a WordPress.com site to a local site. Requires authentication. Specify the remote site URL or ID and sync options.
- site_import: Import a backup file (.zip, .tar.gz, .sql, .wpress) into a local site.
- site_export: Export a local site to a backup file. Supports full-site (.zip, .tar.gz) or database-only (.sql) exports.

## General rules

Expand Down
171 changes: 170 additions & 1 deletion apps/cli/ai/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,17 @@ import { validateBlocks, type ValidationReport } from 'cli/ai/block-validator';
import { getSharedBrowser } from 'cli/ai/browser-utils';
import { auditPerformance } from 'cli/ai/performance-audit';
import { createWpcomToolDefinitions } from 'cli/ai/wpcom-tools';
import { runCommand as runExportCommand } from 'cli/commands/export';
import { runCommand as runImportCommand } from 'cli/commands/import';
import { runCommand as runCreatePreviewCommand } from 'cli/commands/preview/create';
import {
Mode as PreviewDeleteMode,
runCommand as runDeletePreviewCommand,
} from 'cli/commands/preview/delete';
import { runCommand as runListPreviewCommand } from 'cli/commands/preview/list';
import { runCommand as runUpdatePreviewCommand } from 'cli/commands/preview/update';
import { runCommand as runPullCommand } from 'cli/commands/pull';
import { runCommand as runPushCommand } from 'cli/commands/push';
import { runCommand as runCreateSiteCommand } from 'cli/commands/site/create';
import { runCommand as runDeleteSiteCommand } from 'cli/commands/site/delete';
import { runCommand as runListSitesCommand } from 'cli/commands/site/list';
Expand All @@ -25,6 +29,7 @@ import { getSiteByFolder, getSiteUrl } from 'cli/lib/cli-config/sites';
import { connectToDaemon, disconnectFromDaemon } from 'cli/lib/daemon-client';
import { getUnsupportedWpCliPostContentMessage } from 'cli/lib/rewrite-wp-cli-post-content';
import { STUDIO_SITES_ROOT } from 'cli/lib/site-paths';
import { parseSyncOptions } from 'cli/lib/sync-api';
import { normalizeHostname } from 'cli/lib/utils';
import { isServerRunning, sendWpCliCommand } from 'cli/lib/wordpress-server-manager';
import { getProgressCallback, setProgressCallback, emitProgress } from 'cli/logger';
Expand Down Expand Up @@ -777,6 +782,166 @@ const auditPerformanceTool = tool(
}
);

const pushSiteTool = tool(
'site_push',
'Pushes a local WordPress site to a WordPress.com site. Requires WordPress.com authentication ' +
'(studio auth login). Exports the local site, uploads it, and imports it on the remote site. ' +
'This can take several minutes depending on site size.',
{
nameOrPath: z.string().describe( 'The local site name or file system path' ),
remoteSite: z
.string()
.describe( 'The remote WordPress.com site URL or numeric site ID to push to' ),
options: z
.string()
.optional()
.describe(
'Comma-separated sync options: all, sqls, uploads, plugins, themes, contents. Defaults to "all".'
),
},
async ( args ) => {
try {
const site = await resolveSite( args.nameOrPath );
const syncOptions = parseSyncOptions( args.options ?? 'all' );

const result = await captureCommandOutput( () =>
runPushCommand( site.path, syncOptions, args.remoteSite )
);
const output = result.consoleOutput || result.progressOutput || 'Push completed.';

if ( result.exitCode ) {
return errorResult( output );
}

return textResult( output );
} catch ( error ) {
return errorResult(
`Failed to push site: ${ error instanceof Error ? error.message : String( error ) }`
);
}
}
);

const pullSiteTool = tool(
'site_pull',
'Pulls a WordPress.com site to a local WordPress site. Requires WordPress.com authentication ' +
'(studio auth login). Creates a remote backup, downloads it, and imports it locally. ' +
'This can take several minutes depending on site size.',
{
nameOrPath: z.string().describe( 'The local site name or file system path' ),
remoteSite: z
.string()
.describe( 'The remote WordPress.com site URL or numeric site ID to pull from' ),
options: z
.string()
.optional()
.describe(
'Comma-separated sync options: all, sqls, uploads, plugins, themes, contents. Defaults to "all".'
),
},
async ( args ) => {
try {
const site = await resolveSite( args.nameOrPath );
const syncOptions = parseSyncOptions( args.options ?? 'all' );

const result = await captureCommandOutput( () =>
runPullCommand( site.path, syncOptions, args.remoteSite )
);
const output = result.consoleOutput || result.progressOutput || 'Pull completed.';

if ( result.exitCode ) {
return errorResult( output );
}

return textResult( output );
} catch ( error ) {
return errorResult(
`Failed to pull site: ${ error instanceof Error ? error.message : String( error ) }`
);
}
}
);

const importSiteTool = tool(
'site_import',
'Imports a backup file into a local WordPress site. Supports .zip, .tar.gz, .sql, and .wpress formats. ' +
'The site server will be stopped during import and restarted afterward if it was running.',
{
nameOrPath: z.string().describe( 'The local site name or file system path' ),
importFile: z.string().describe( 'Absolute path to the backup file to import' ),
},
async ( args ) => {
try {
const site = await resolveSite( args.nameOrPath );

const result = await captureCommandOutput( () =>
runImportCommand( site.path, args.importFile )
);
const output = result.consoleOutput || result.progressOutput || 'Import completed.';

if ( result.exitCode ) {
return errorResult( output );
}

return textResult( output );
} catch ( error ) {
return errorResult(
`Failed to import site: ${ error instanceof Error ? error.message : String( error ) }`
);
}
}
);

const exportSiteTool = tool(
'site_export',
'Exports a local WordPress site to a backup file. Supports full-site export (.zip or .tar.gz) ' +
'or database-only export (.sql). If no export file path is provided, creates a timestamped file in the current directory.',
{
nameOrPath: z.string().describe( 'The local site name or file system path' ),
exportFile: z
.string()
.optional()
.describe(
'Path for the export file. Use .zip or .tar.gz for full export, .sql for database only. ' +
'If omitted, a timestamped file is created in the current directory.'
),
mode: z
.enum( [ 'full', 'db' ] )
.optional()
.describe(
'Export mode: "full" for entire site, "db" for database only. Defaults to "full".'
),
},
async ( args ) => {
try {
const site = await resolveSite( args.nameOrPath );
const mode = args.mode ?? 'full';

let exportFile = args.exportFile;
if ( ! exportFile ) {
const timestamp = new Date().toISOString().replace( /[:.]/g, '-' ).slice( 0, 19 );
const ext = mode === 'db' ? '.sql' : '.zip';
exportFile = path.join( process.cwd(), `studio-backup-${ timestamp }${ ext }` );
}

const result = await captureCommandOutput( () =>
runExportCommand( site.path, exportFile, mode )
);
const output = result.consoleOutput || result.progressOutput || `Exported to ${ exportFile }`;

if ( result.exitCode ) {
return errorResult( output );
}

return textResult( output );
} catch ( error ) {
return errorResult(
`Failed to export site: ${ error instanceof Error ? error.message : String( error ) }`
);
}
}
);

export const studioToolDefinitions = [
createSiteTool,
listSitesTool,
Expand All @@ -793,6 +958,10 @@ export const studioToolDefinitions = [
takeScreenshotTool,
installTaxonomyScriptsTool,
auditPerformanceTool,
pushSiteTool,
pullSiteTool,
importSiteTool,
exportSiteTool,
];

export function createStudioTools() {
Expand All @@ -812,6 +981,6 @@ export function createRemoteSiteTools( token: string, siteId: number ) {
return createSdkMcpServer( {
name: 'studio',
version: '1.0.0',
tools: [ ...wpcomTools, takeScreenshotTool ],
tools: [ ...wpcomTools, takeScreenshotTool, createSiteTool, pullSiteTool ],
} );
}
25 changes: 19 additions & 6 deletions apps/cli/lib/sync-site-picker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,31 @@ import { normalizeHostname } from 'cli/lib/utils';
import { LoggerError } from 'cli/logger';
import type { SyncSite } from '@studio/common/types/sync';

function throwSyncSupportError( site: SyncSite ): never {
if ( site.syncSupport === 'needs-transfer' ) {
throw new LoggerError(
sprintf(
__(
'Site %1$s requires hosting features to be enabled. Please visit https://wordpress.com/hosting-features/%2$d to activate them, then try again.'
),
site.name,
site.id
)
);
}
throw new LoggerError(
sprintf( __( 'Site %s is not syncable (%s)' ), site.name, site.syncSupport )
);
}

export function findSyncSiteByIdentifier( sites: SyncSite[], identifier: string ): SyncSite {
// Try numeric ID match first
const numericId = Number( identifier );
if ( ! isNaN( numericId ) ) {
const site = sites.find( ( s ) => s.id === numericId );
if ( site ) {
if ( site.syncSupport !== 'syncable' ) {
throw new LoggerError(
sprintf( __( 'Site %s is not syncable (%s)' ), site.name, site.syncSupport )
);
throwSyncSupportError( site );
}
return site;
}
Expand All @@ -40,9 +55,7 @@ export function findSyncSiteByIdentifier( sites: SyncSite[], identifier: string

const site = matched[ 0 ];
if ( site.syncSupport !== 'syncable' ) {
throw new LoggerError(
sprintf( __( 'Site %s is not syncable (%s)' ), site.name, site.syncSupport )
);
throwSyncSupportError( site );
}

return site;
Expand Down
Loading