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
67 changes: 67 additions & 0 deletions apps/cli/commands/export.command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import path from 'path';
import { __ } from '@wordpress/i18n';
import { untildify } from 'cli/lib/utils';
import { StudioArgv } from 'cli/types';

export const builder = ( yargs: StudioArgv ) => {
return yargs
.positional( 'export-file', {
type: 'string',
normalize: true,
demandOption: false,
description: __(
'Path to the export file. All exports can use .zip or .tar.gz. Database-only exports can also use .sql.'
),
coerce: ( value ) => {
return path.resolve( untildify( value ) );
},
} )
.option( 'mode', {
type: 'string',
choices: [ 'full', 'content', 'db' ] as const,
default: 'full' as const,
description: __(
'Export the full site, just the content, or just the database. Default exports full site.'
),
} )
.option( 'split-db-dump-by-table', {
type: 'boolean',
default: false,
description: __( 'Split the database dump by table' ),
hidden: true,
} )
.option( 'include-only', {
type: 'array',
description: __( 'Include only the specified paths in the export' ),
coerce: ( value ) => {
if ( ! Array.isArray( value ) ) {
throw new Error( __( 'include-only must be an array' ) );
}
return value.map( String );
},
hidden: true,
} )
.option( 'apply-deploy-ignore', {
type: 'boolean',
default: false,
description: __( 'Apply .deployignore patterns when exporting' ),
hidden: true,
} )
.option( 'suppress-tracks-event', {
type: 'boolean',
default: false,
hidden: true,
} );
};

export const registerCommand = ( yargs: StudioArgv ) => {
return yargs.command( {
command: 'export [export-file]',
describe: __( 'Export site to a backup file' ),
builder,
handler: async ( argv ) => {
const { commandHandler } = await import( 'cli/commands/export' );
return commandHandler( argv );
},
} );
};
144 changes: 43 additions & 101 deletions apps/cli/commands/export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ import { ExportOptions } from 'cli/lib/import-export/export/types';
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 { classifyExportFailure } from 'cli/lib/utils';
import { Logger, LoggerError } from 'cli/logger';
import { StudioArgv } from 'cli/types';
import type { builder } from 'cli/commands/export.command';
import type { CommandArgs } from 'cli/types';

const defaultLogger = new Logger< LoggerAction >();

Expand Down Expand Up @@ -261,103 +262,44 @@ function getTimestamp( date = new Date() ): string {
.map( ( part ) => String( part ).padStart( 2, '0' ) )
.join( '-' );
}
export async function commandHandler( argv: CommandArgs< typeof builder > ) {
try {
let exportFile: string;
const timestamp = getTimestamp();

export const registerCommand = ( yargs: StudioArgv ) => {
return yargs.command( {
command: 'export [export-file]',
describe: __( 'Export site to a backup file' ),
builder: ( yargs ) => {
return yargs
.positional( 'export-file', {
type: 'string',
normalize: true,
demandOption: false,
description: __(
'Path to the export file. All exports can use .zip or .tar.gz. Database-only exports can also use .sql.'
),
coerce: ( value ) => {
return path.resolve( untildify( value ) );
},
} )
.option( 'mode', {
type: 'string',
choices: [ 'full', 'content', 'db' ] as const,
default: 'full' as const,
description: __(
'Export the full site, just the content, or just the database. Default exports full site.'
),
} )
.option( 'split-db-dump-by-table', {
type: 'boolean',
default: false,
description: __( 'Split the database dump by table' ),
hidden: true,
} )
.option( 'include-only', {
type: 'array',
description: __( 'Include only the specified paths in the export' ),
coerce: ( value ) => {
if ( ! Array.isArray( value ) ) {
throw new Error( __( 'include-only must be an array' ) );
}
return value.map( String );
},
hidden: true,
} )
.option( 'apply-deploy-ignore', {
type: 'boolean',
default: false,
description: __( 'Apply .deployignore patterns when exporting' ),
hidden: true,
} )
.option( 'suppress-tracks-event', {
type: 'boolean',
default: false,
hidden: true,
} );
},
handler: async ( argv ) => {
try {
let exportFile: string;
const timestamp = getTimestamp();

if ( argv.exportFile ) {
exportFile = argv.exportFile;
} else if ( argv.mode === 'full' || argv.mode === 'content' ) {
exportFile = path.join( process.cwd(), `studio-backup-${ timestamp }.zip` );
} else {
exportFile = path.join( process.cwd(), `studio-backup-${ timestamp }.sql` );
}

if (
( argv.mode === 'full' || argv.mode === 'content' ) &&
! exportFile.endsWith( '.zip' ) &&
! exportFile.endsWith( '.tar.gz' )
) {
throw new LoggerError(
__(
'Invalid export file extension. Must be .zip or .tar.gz when exporting the full site.'
)
);
}

await runCommand(
argv.path,
exportFile,
argv.mode,
argv.splitDbDumpByTable,
argv.includeOnly,
argv.applyDeployIgnore,
argv.suppressTracksEvent
);
} catch ( error ) {
if ( error instanceof LoggerError ) {
defaultLogger.reportError( error );
} else {
const loggerError = new LoggerError( __( 'Failed to export site' ), error );
defaultLogger.reportError( loggerError );
}
}
},
} );
};
if ( argv.exportFile ) {
exportFile = argv.exportFile;
} else if ( argv.mode === 'full' || argv.mode === 'content' ) {
exportFile = path.join( process.cwd(), `studio-backup-${ timestamp }.zip` );
} else {
exportFile = path.join( process.cwd(), `studio-backup-${ timestamp }.sql` );
}

if (
( argv.mode === 'full' || argv.mode === 'content' ) &&
! exportFile.endsWith( '.zip' ) &&
! exportFile.endsWith( '.tar.gz' )
) {
throw new LoggerError(
__( 'Invalid export file extension. Must be .zip or .tar.gz when exporting the full site.' )
);
}

await runCommand(
argv.path,
exportFile,
argv.mode,
argv.splitDbDumpByTable,
argv.includeOnly,
argv.applyDeployIgnore,
argv.suppressTracksEvent
);
} catch ( error ) {
if ( error instanceof LoggerError ) {
defaultLogger.reportError( error );
} else {
const loggerError = new LoggerError( __( 'Failed to export site' ), error );
defaultLogger.reportError( loggerError );
}
}
}
39 changes: 39 additions & 0 deletions apps/cli/commands/import.command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import path from 'path';
import { __ } from '@wordpress/i18n';
import { untildify } from 'cli/lib/utils';
import { StudioArgv } from 'cli/types';

export const builder = ( yargs: StudioArgv ) => {
return yargs
.positional( 'import-file', {
type: 'string',
normalize: true,
demandOption: true,
description: __( 'Path to the import file' ),
coerce: ( value ) => {
return path.resolve( untildify( value ) );
},
} )
.option( 'start-server', {
type: 'boolean',
default: false,
hidden: true,
} )
.option( 'suppress-tracks-event', {
type: 'boolean',
default: false,
hidden: true,
} );
};

export const registerCommand = ( yargs: StudioArgv ) => {
return yargs.command( {
command: 'import <import-file>',
describe: __( 'Import a backup file to site' ),
builder,
handler: async ( argv ) => {
const { commandHandler } = await import( 'cli/commands/import' );
return commandHandler( argv );
},
} );
};
58 changes: 15 additions & 43 deletions apps/cli/commands/import.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,15 @@ 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';
import { classifyImportFailure } from 'cli/lib/utils';
import {
isServerRunning,
startWordPressServer,
stopWordPressServer,
} from 'cli/lib/wordpress-server-manager';
import { Logger, LoggerError } from 'cli/logger';
import { StudioArgv } from 'cli/types';
import type { builder } from 'cli/commands/import.command';
import type { CommandArgs } from 'cli/types';

const defaultLogger = new Logger< LoggerAction >();

Expand Down Expand Up @@ -447,44 +448,15 @@ async function recordSiteImportEvent( props: {
// Best-effort telemetry — never block or fail the import.
}
}

export const registerCommand = ( yargs: StudioArgv ) => {
return yargs.command( {
command: 'import <import-file>',
describe: __( 'Import a backup file to site' ),
builder: ( yargs ) => {
return yargs
.positional( 'import-file', {
type: 'string',
normalize: true,
demandOption: true,
description: __( 'Path to the import file' ),
coerce: ( value ) => {
return path.resolve( untildify( value ) );
},
} )
.option( 'start-server', {
type: 'boolean',
default: false,
hidden: true,
} )
.option( 'suppress-tracks-event', {
type: 'boolean',
default: false,
hidden: true,
} );
},
handler: async ( argv ) => {
try {
await runCommand( argv.path, argv.importFile, argv.startServer, argv.suppressTracksEvent );
} catch ( error ) {
if ( error instanceof LoggerError ) {
defaultLogger.reportError( error );
} else {
const loggerError = new LoggerError( __( 'Failed to import site' ), error );
defaultLogger.reportError( loggerError );
}
}
},
} );
};
export async function commandHandler( argv: CommandArgs< typeof builder > ) {
try {
await runCommand( argv.path, argv.importFile, argv.startServer, argv.suppressTracksEvent );
} catch ( error ) {
if ( error instanceof LoggerError ) {
defaultLogger.reportError( error );
} else {
const loggerError = new LoggerError( __( 'Failed to import site' ), error );
defaultLogger.reportError( loggerError );
}
}
}
18 changes: 18 additions & 0 deletions apps/cli/commands/mcp.command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { __ } from '@wordpress/i18n';
import { StudioArgv } from 'cli/types';

export const builder = ( yargs: StudioArgv ) => {
return yargs.help( false ).option( 'help', { type: 'boolean' } );
};

export const registerCommand = ( yargs: StudioArgv ) => {
return yargs.command( {
command: 'mcp',
describe: __( 'MCP server for AI agents' ),
builder,
handler: async ( argv ) => {
const { commandHandler } = await import( 'cli/commands/mcp' );
return commandHandler( argv );
},
} );
};
Loading
Loading