Skip to content

Commit c712b29

Browse files
katinthehatsiteKateryna Kodonenko
andauthored
Implement the /publish slash command to push local sites to WP.com (#4043)
## Related issues <!-- Link a related issue to this PR. If the PR does not immediately resolve the issue, for example, it requires a separate deployment to production, avoid using the "Fixes" keyword and use "Related to" instead. --> Fixes STU-1552 ## How AI was used in this PR <!-- Help reviewers understand what to look for and verify that you've reviewed the code yourself. --> ## Proposed Changes <!-- Explain the intent of this PR: - What problem does it solve, or what need does it address? - How does it affect the user (new capability, fix, performance, accessibility, etc.)? - Note any user-visible behavior changes or trade-offs. Focus on the "why" and the user impact. Avoid listing modified files or describing implementation mechanics — the diff already shows that. --> This PR adds a `/publish` command for publishing local sites to WP.com. ## Testing Instructions <!-- Add as many details as possible to help others reproduce the issue and test the fix. "Before / After" screenshots can also be very helpful when the change is visual. --> * Pull the changes from this branch * Build CLI with `npm run cli:build` * Launch Studio code with `node apps/cli/dist/cli/main.mjs code` * Ensure that you have no sites selected first * Type `publish` * Confirm that you are encouraged to select a site <img width="738" height="204" alt="Screenshot 2026-07-02 at 10 46 23 AM" src="https://github.com/user-attachments/assets/70596d4a-6a28-40ca-bdc6-d1f7afd37482" /> * Once the site is selected, type `/publish` again * Select a remote site to publish to * Start push process * Confirm that it goes through * Then, test with `/publish` command for an account that does not have any remote sites (you can use this diff and will need to rebuild CLI): ```diff --git a/apps/cli/ai/slash-commands.ts b/apps/cli/ai/slash-commands.ts index e56a72b..ebf34e5de 100644 --- a/apps/cli/ai/slash-commands.ts +++ b/apps/cli/ai/slash-commands.ts @@ -515,7 +515,7 @@ export const AI_CHAT_SLASH_COMMANDS: SlashCommandDef[] = [ ctx.ui.showProgress( __( 'Fetching your WordPress.com sites…' ) ); ctx.ui.setBusy( true ); - const remoteSites = await fetchSyncableSites( token.accessToken ); + const remoteSites: Awaited< ReturnType< typeof fetchSyncableSites > > = []; // TODO: remove mock const syncable = remoteSites.filter( ( s ) => s.syncSupport === 'syncable' ); if ( syncable.length === 0 ) { ``` * Confirm it opens a link in the browser if you have no WP.com sites: <img width="751" height="243" alt="Screenshot 2026-07-02 at 11 23 36 AM" src="https://github.com/user-attachments/assets/bdc9f6cb-371e-491b-bc99-f3a055197f79" /> * Try using the `/publish` command when you are logged out and confirm that you are asked to login first ## Pre-merge Checklist <!-- Complete applicable items on this checklist **before** merging into trunk. Inapplicable items can be left unchecked. Both the PR author and reviewer are responsible for ensuring the checklist is completed. --> - [ ] Have you checked for TypeScript, React or other console errors? --------- Co-authored-by: Kateryna Kodonenko <kateryna@automattic.com>
1 parent a6aa1dc commit c712b29

1 file changed

Lines changed: 94 additions & 0 deletions

File tree

‎apps/cli/ai/slash-commands.ts‎

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@ import { runCommand as runLoginCommand } from 'cli/commands/auth/login';
1010
import { runCommand as runLogoutCommand } from 'cli/commands/auth/logout';
1111
import { runCommand as runCreatePreviewCommand } from 'cli/commands/preview/create';
1212
import { runCommand as runUpdatePreviewCommand } from 'cli/commands/preview/update';
13+
import { runCommand as runPushCommand } from 'cli/commands/push';
1314
import { openBrowser } from 'cli/lib/browser';
1415
import { getSnapshotsFromConfig, isSnapshotExpired } from 'cli/lib/snapshots';
16+
import { fetchSyncableSites } from 'cli/lib/sync-api';
1517
import { LoggerError } from 'cli/logger';
1618
import { loadRemoteSessionConfig } from 'cli/remote-session/config';
1719
import { DaemonAlreadyRunningError, startDaemon, stopDaemon } from 'cli/remote-session/daemon';
@@ -493,6 +495,98 @@ export const AI_CHAT_SLASH_COMMANDS: SlashCommandDef[] = [
493495
return 'continue';
494496
},
495497
},
498+
{
499+
name: 'publish',
500+
description: __( 'Publish the active site to WordPress.com' ),
501+
handler: async ( _prompt, ctx ) => {
502+
const site = ctx.ui.activeSite;
503+
if ( ! site ) {
504+
ctx.ui.showInfo( __( 'No site selected. Use ↓ to select a site first.' ) );
505+
return 'continue';
506+
}
507+
508+
const token = await readAuthToken();
509+
if ( ! token ) {
510+
ctx.ui.showInfo( __( 'WordPress.com login required. Use /login to authenticate.' ) );
511+
return 'continue';
512+
}
513+
514+
try {
515+
ctx.ui.showProgress( __( 'Fetching your WordPress.com sites…' ) );
516+
ctx.ui.setBusy( true );
517+
518+
const remoteSites = await fetchSyncableSites( token.accessToken );
519+
const syncable = remoteSites.filter( ( s ) => s.syncSupport === 'syncable' );
520+
521+
if ( syncable.length === 0 ) {
522+
ctx.ui.setBusy( false );
523+
const checkoutUrl = new URL( 'https://wordpress.com/setup/new-hosted-site' );
524+
checkoutUrl.searchParams.set( 'ref', 'studio' );
525+
checkoutUrl.searchParams.set( 'section', 'studio-sync' );
526+
checkoutUrl.searchParams.set( 'showDomainStep', 'true' );
527+
checkoutUrl.searchParams.set( 'new', site.name );
528+
529+
await openBrowser( checkoutUrl.toString() );
530+
ctx.ui.showInfo(
531+
__(
532+
'No WordPress.com sites found. Opening WordPress.com to create one — once your site is ready, run /publish again.'
533+
)
534+
);
535+
return 'continue';
536+
}
537+
538+
// Let the user pick which site to publish to.
539+
const siteOptions = syncable.map( ( s ) => ( {
540+
label: s.name,
541+
description: s.url,
542+
} ) );
543+
ctx.ui.setBusy( false );
544+
const answer = await ctx.ui.askUser( [
545+
{
546+
question: __( 'Select a WordPress.com site to publish to' ),
547+
options: siteOptions,
548+
},
549+
] );
550+
const selectedName = Object.values( answer )[ 0 ] as string;
551+
const remoteSite = syncable.find( ( s ) => s.name === selectedName );
552+
if ( ! remoteSite ) {
553+
ctx.ui.showInfo( __( 'No site selected.' ) );
554+
return 'continue';
555+
}
556+
557+
ctx.ui.showProgress(
558+
sprintf( __( 'Publishing to %s… this may take several minutes.' ), remoteSite.name )
559+
);
560+
ctx.ui.setBusy( true );
561+
562+
const result = await captureCommandOutput( () =>
563+
runPushCommand( site.path, [ 'all' ], String( remoteSite.id ) )
564+
);
565+
566+
ctx.ui.setBusy( false );
567+
568+
if ( result.exitCode ) {
569+
ctx.ui.showError( result.consoleOutput || __( 'Failed to publish site.' ) );
570+
} else {
571+
ctx.ui.showSuccess(
572+
sprintf( __( 'Published to %s' ), remoteSite.url ) + '\n\n ' + remoteSite.url
573+
);
574+
}
575+
} catch ( error ) {
576+
ctx.ui.setBusy( false );
577+
if ( isPromptAbortError( error ) ) {
578+
ctx.ui.showInfo( __( 'Publish canceled.' ) );
579+
return 'continue';
580+
}
581+
if ( error instanceof LoggerError ) {
582+
ctx.ui.showError( error.message );
583+
} else {
584+
ctx.ui.showError( __( 'Failed to publish site.' ) );
585+
}
586+
}
587+
return 'continue';
588+
},
589+
},
496590
{
497591
name: 'remote-session',
498592
description: __( 'Manage the Telegram remote-session daemon (start, stop)' ),

0 commit comments

Comments
 (0)