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
20 changes: 15 additions & 5 deletions apps/cli/ai/tests/tools.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { vi } from 'vitest';
import { runCommand as runCreatePreviewCommand } from 'cli/commands/preview/create';
import { runCommand as runDeletePreviewCommand } from 'cli/commands/preview/delete';
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 { readCliConfig } from 'cli/lib/cli-config/core';
Expand All @@ -20,9 +23,13 @@ vi.mock( 'cli/commands/preview/create', () => ( {
runCommand: vi.fn(),
} ) );

vi.mock( 'cli/commands/preview/delete', () => ( {
runCommand: vi.fn(),
} ) );
vi.mock( import( 'cli/commands/preview/delete' ), async ( importActual ) => {
const actual = await importActual();
return {
Mode: actual.Mode,
runCommand: vi.fn(),
};
} );

vi.mock( 'cli/commands/preview/list', () => ( {
runCommand: vi.fn(),
Expand Down Expand Up @@ -180,7 +187,10 @@ describe( 'Studio AI MCP tools', () => {
null
);

expect( runDeletePreviewCommand ).toHaveBeenCalledWith( 'demo.wordpress.com' );
expect( runDeletePreviewCommand ).toHaveBeenCalledWith(
PreviewDeleteMode.DELETE_SINGLE_SNAPSHOT,
'demo.wordpress.com'
);
expect( result.isError ).toBe( true );
expect( getTextContent( result ) ).toBe( 'Failed to delete preview site' );
} );
Expand Down
7 changes: 5 additions & 2 deletions apps/cli/ai/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import { z } from 'zod/v4';
import { validateBlocks, type ValidationReport } from 'cli/ai/block-validator';
import { getSharedBrowser } from 'cli/ai/browser-utils';
import { runCommand as runCreatePreviewCommand } from 'cli/commands/preview/create';
import { runCommand as runDeletePreviewCommand } from 'cli/commands/preview/delete';
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 runCreateSiteCommand } from 'cli/commands/site/create';
Expand Down Expand Up @@ -426,7 +429,7 @@ const deletePreviewTool = tool(
async ( args ) => {
const normalizedHost = normalizeHostname( args.host );
return runPreviewCommand(
() => runDeletePreviewCommand( normalizedHost ),
() => runDeletePreviewCommand( PreviewDeleteMode.DELETE_SINGLE_SNAPSHOT, normalizedHost ),
`Preview site "${ normalizedHost }" deleted.`,
'Failed to delete preview site'
);
Expand Down
62 changes: 37 additions & 25 deletions apps/cli/commands/_events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,7 @@ import {
SITE_EVENTS,
SNAPSHOT_EVENTS,
siteDetailsSchema,
siteSocketEventSchema,
snapshotSocketEventSchema,
authSocketEventSchema,
socketEventSchema,
SiteEvent,
SnapshotEvent,
AuthEvent,
Expand Down Expand Up @@ -90,16 +88,26 @@ function emitAuthEvent( event: AUTH_EVENTS, token?: AuthEvent[ 'token' ] ): void
logger.reportKeyValuePair( 'auth-event', JSON.stringify( payload ) );
}

const emitSnapshotEvent = sequential(
async ( event: SNAPSHOT_EVENTS, snapshotUrl: string ): Promise< void > => {
const emitDeletedAllSnapshotsEvent = sequential(
async ( event: SNAPSHOT_EVENTS.DELETED_ALL ): Promise< void > => {
const payload: SnapshotEvent = { event };
logger.reportKeyValuePair( 'snapshot-event', JSON.stringify( payload ) );
}
);

const emitSingleSnapshotEvent = sequential(
async (
event: SNAPSHOT_EVENTS.CREATED | SNAPSHOT_EVENTS.UPDATED | SNAPSHOT_EVENTS.DELETED,
snapshotUrl: string
): Promise< void > => {
const cliConfig = await readCliConfig();

const snapshot = cliConfig.snapshots.find( ( s ) => s.url === snapshotUrl );
const payload: SnapshotEvent = {
event,
snapshotUrl,
snapshot: snapshot ?? undefined,
};

logger.reportKeyValuePair( 'snapshot-event', JSON.stringify( payload ) );
}
);
Expand All @@ -108,25 +116,29 @@ export async function runCommand(): Promise< void > {
const eventsSocketServer = new SocketServer( SITE_EVENTS_SOCKET_PATH, 2500 );
eventsSocketServer.on( 'message', ( { message: packet } ) => {
try {
const authParsed = authSocketEventSchema.safeParse( packet );
if ( authParsed.success ) {
emitAuthEvent( authParsed.data.event, authParsed.data.data.token );
return;
}

const snapshotParsed = snapshotSocketEventSchema.safeParse( packet );
if ( snapshotParsed.success ) {
void emitSnapshotEvent( snapshotParsed.data.event, snapshotParsed.data.data.snapshotUrl );
return;
}

const parsedPacket = siteSocketEventSchema.parse( packet );
if (
parsedPacket.event === SITE_EVENTS.CREATED ||
parsedPacket.event === SITE_EVENTS.UPDATED ||
parsedPacket.event === SITE_EVENTS.DELETED
) {
void emitSiteEvent( parsedPacket.event, parsedPacket.data.siteId );
const parsed = socketEventSchema.parse( packet );

switch ( parsed.event ) {
case AUTH_EVENTS.LOGIN:
case AUTH_EVENTS.LOGOUT:
void emitAuthEvent( parsed.event, parsed.data.token );
break;

case SNAPSHOT_EVENTS.CREATED:
case SNAPSHOT_EVENTS.UPDATED:
case SNAPSHOT_EVENTS.DELETED:
void emitSingleSnapshotEvent( parsed.event, parsed.data.snapshotUrl );
break;

case SNAPSHOT_EVENTS.DELETED_ALL:
void emitDeletedAllSnapshotsEvent( parsed.event );
break;

case SITE_EVENTS.CREATED:
case SITE_EVENTS.UPDATED:
case SITE_EVENTS.DELETED:
void emitSiteEvent( parsed.event, parsed.data.siteId );
break;
}
} catch ( error ) {
// Do nothing
Expand Down
93 changes: 67 additions & 26 deletions apps/cli/commands/preview/delete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,44 +2,71 @@ import { SNAPSHOT_EVENTS } from '@studio/common/lib/cli-events';
import { readAuthToken } from '@studio/common/lib/shared-config';
import { PreviewCommandLoggerAction as LoggerAction } from '@studio/common/logger-actions';
import { __ } from '@wordpress/i18n';
import { deleteSnapshot } from 'cli/lib/api';
import { deleteAllSnapshots, deleteSnapshot } from 'cli/lib/api';
import { deleteAllSnapshotsForUserFromConfig } from 'cli/lib/cli-config/snapshots';
import { emitCliEvent } from 'cli/lib/daemon-client';
import { deleteSnapshotFromConfig, getSnapshotsFromConfig } from 'cli/lib/snapshots';
import { normalizeHostname } from 'cli/lib/utils';
import { Logger, LoggerError } from 'cli/logger';
import { StudioArgv } from 'cli/types';

export async function runCommand( host: string ): Promise< void > {
export enum Mode {
DELETE_SINGLE_SNAPSHOT,
DELETE_ALL_SNAPSHOT,
}

export async function runCommand(
mode: Mode.DELETE_SINGLE_SNAPSHOT,
host: string
): Promise< void >;
export async function runCommand(
mode: Mode.DELETE_ALL_SNAPSHOT,
host: undefined
): Promise< void >;
export async function runCommand( mode: Mode, host: string | undefined ): Promise< void > {
const logger = new Logger< LoggerAction >();

try {
logger.reportStart( LoggerAction.VALIDATE, __( 'Validating…' ) );
const token = await readAuthToken();
if ( ! token ) {
throw new LoggerError(
__( 'Authentication required. Please log in with `studio auth login`.' )
);
}

const snapshots = await getSnapshotsFromConfig( token.id );
const snapshotToDelete = snapshots.find( ( s ) => s.url === host );
if ( ! snapshotToDelete ) {
throw new LoggerError(
__(
'Preview site not found. ' +
'Use the `studio preview list` command to see available preview sites.'
)
);
}
logger.reportSuccess( __( 'Validation successful' ), true );

logger.reportStart( LoggerAction.DELETE, __( 'Deleting…' ) );
await deleteSnapshot( snapshotToDelete.atomicSiteId, token.accessToken );
await deleteSnapshotFromConfig( snapshotToDelete.url );
await emitCliEvent( {
event: SNAPSHOT_EVENTS.DELETED,
data: { snapshotUrl: snapshotToDelete.url },
} );
logger.reportSuccess( __( 'Deletion successful' ) );
if ( mode === Mode.DELETE_SINGLE_SNAPSHOT ) {
logger.reportStart( LoggerAction.VALIDATE, __( 'Validating…' ) );

const snapshotToDelete = snapshots.find( ( s ) => s.url === host );
if ( ! snapshotToDelete ) {
throw new LoggerError(
__(
'Preview site not found. ' +
'Use the `studio preview list` command to see available preview sites.'
)
);
}
logger.reportSuccess( __( 'Validation successful' ), true );

logger.reportStart( LoggerAction.DELETE, __( 'Deleting…' ) );
await deleteSnapshot( snapshotToDelete.atomicSiteId, token.accessToken );
await deleteSnapshotFromConfig( snapshotToDelete.url );
await emitCliEvent( {
event: SNAPSHOT_EVENTS.DELETED,
data: { snapshotUrl: snapshotToDelete.url },
} );
logger.reportSuccess( __( 'Deletion successful' ) );
} else {
logger.reportStart( LoggerAction.DELETE_ALL, __( 'Deleting all preview sites…' ) );

await deleteAllSnapshots( token.accessToken );
await deleteAllSnapshotsForUserFromConfig( token.id );
await emitCliEvent( { event: SNAPSHOT_EVENTS.DELETED_ALL } );

logger.reportSuccess( __( 'Deletion successful' ) );
}
} catch ( error ) {
if ( error instanceof LoggerError ) {
logger.reportError( error );
Expand All @@ -52,22 +79,36 @@ export async function runCommand( host: string ): Promise< void > {

export const registerCommand = ( yargs: StudioArgv ) => {
return yargs.command( {
command: 'delete <host>',
describe: __( 'Delete a preview site' ),
command: 'delete [host] [--all]',
describe: __( 'Delete preview site(s)' ),
builder: ( yargs ) => {
return yargs
.option( 'all', {
type: 'boolean',
describe: __( 'Delete all preview sites for your user' ),
default: false,
} )
.positional( 'host', {
type: 'string',
description: __( 'Hostname of the preview site to delete' ),
demandOption: true,
} )
.check( ( argv ) => {
if ( ! argv.all && ! argv.host ) {
throw new Error( __( 'Hostname is required unless --all is passed.' ) );
}
return true;
} )
.option( 'path', {
hidden: true,
} );
},
handler: async ( argv ) => {
const normalizedHost = normalizeHostname( argv.host );
await runCommand( normalizedHost );
if ( argv.all ) {
await runCommand( Mode.DELETE_ALL_SNAPSHOT, undefined );
} else if ( argv.host ) {
const normalizedHost = normalizeHostname( argv.host );
await runCommand( Mode.DELETE_SINGLE_SNAPSHOT, normalizedHost );
}
},
} );
};
Loading
Loading