Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
fd24a5f
Implement studio site stop CLI command with graceful shutdown
bcotrim Nov 26, 2025
25bd8ce
Fix types and remove sendStopMessage
fredrikekelund Nov 27, 2025
e2bc082
stop proxy if needed
bcotrim Nov 27, 2025
fc01870
use getSiteByFolder
bcotrim Nov 27, 2025
57a05ba
Fix issues from my previous commits
fredrikekelund Nov 28, 2025
1a81ea8
Clean up test file
fredrikekelund Nov 28, 2025
a4b15b7
fix lint and unit tests
bcotrim Nov 28, 2025
cb20e2d
CLI: Implement `studio site set-domain` and `studio site set-https`
fredrikekelund Nov 28, 2025
1cdde21
Harden `sendMessage` cleanup logic
fredrikekelund Nov 28, 2025
7c935bb
Cleanup
fredrikekelund Nov 28, 2025
8bdb8c8
Disallow `set-https` command on sites without custom domains
fredrikekelund Nov 28, 2025
97b6f20
Finish early if HTTPS config already matches
fredrikekelund Nov 28, 2025
b985d9b
Simplify
fredrikekelund Nov 28, 2025
41d19d6
Merge branch 'trunk' into f26d/cli-site-set-domain-https
fredrikekelund Nov 28, 2025
b9e007e
Fix yargs option config
fredrikekelund Nov 28, 2025
c903ef3
CLI: Implement `studio site set-php-version`
fredrikekelund Nov 28, 2025
e8028ab
Address review comments
fredrikekelund Dec 1, 2025
864c9b4
Disallow setting the same domain as before
fredrikekelund Dec 1, 2025
1c3af77
Merge branch 'trunk' into f26d/cli-site-set-domain-https
fredrikekelund Dec 2, 2025
78ec8f1
Update tests to follow new paradigm
fredrikekelund Dec 2, 2025
01a7988
Merge branch 'f26d/cli-site-set-domain-https' into f26d/cli-set-php-v…
fredrikekelund Dec 2, 2025
c8ac692
Adopt new test paradigm
fredrikekelund Dec 2, 2025
14ffc2b
Merge branch 'trunk' into f26d/cli-set-php-version
fredrikekelund Dec 2, 2025
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
104 changes: 104 additions & 0 deletions cli/commands/site/set-domain.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { __, _n } from '@wordpress/i18n';
import { getDomainNameValidationError } from 'common/lib/domains';
import { arePathsEqual } from 'common/lib/fs-utils';
import { SiteCommandLoggerAction as LoggerAction } from 'common/logger-actions';
import {
getSiteByFolder,
lockAppdata,
readAppdata,
saveAppdata,
unlockAppdata,
} from 'cli/lib/appdata';
import { removeDomainFromHosts } from 'cli/lib/hosts-file';
import { connect, disconnect } from 'cli/lib/pm2-manager';
import { setupCustomDomain } from 'cli/lib/site-utils';
import {
isServerRunning,
startWordPressServer,
stopWordPressServer,
} from 'cli/lib/wordpress-server-manager';
import { Logger, LoggerError } from 'cli/logger';
import { StudioArgv } from 'cli/types';

export async function runCommand( sitePath: string, domainName: string ): Promise< void > {
const logger = new Logger< LoggerAction >();

try {
logger.reportStart( LoggerAction.LOAD_SITES, __( 'Loading site…' ) );
const site = await getSiteByFolder( sitePath, false );
logger.reportSuccess( __( 'Site loaded' ) );

try {
await lockAppdata();
const appdata = await readAppdata();

const existingDomains = appdata.sites
.map( ( site ) => site.customDomain )
.filter( ( domain ): domain is string => Boolean( domain ) );
const domainError = getDomainNameValidationError( true, domainName, existingDomains );

if ( domainError ) {
throw new LoggerError( domainError );
}

const site = appdata.sites.find( ( site ) => arePathsEqual( site.path, sitePath ) );
if ( ! site ) {
throw new LoggerError( __( 'The specified folder is not added to Studio.' ) );
}
const oldDomainName = site.customDomain;
site.customDomain = domainName;
await saveAppdata( appdata );

if ( oldDomainName ) {
logger.reportStart(
LoggerAction.REMOVE_DOMAIN_FROM_HOSTS,
__( 'Removing domain from hosts file...' )
);
await removeDomainFromHosts( oldDomainName );
logger.reportSuccess( __( 'Domain removed from hosts file' ) );
}
} finally {
await unlockAppdata();
}

logger.reportStart( LoggerAction.START_DAEMON, __( 'Starting process daemon...' ) );
await connect();
logger.reportSuccess( __( 'Process daemon started' ) );

const runningProcess = await isServerRunning( site.id );

if ( runningProcess ) {
await stopWordPressServer( site.id );
await setupCustomDomain( site, logger );
logger.reportStart( LoggerAction.START_SITE, __( 'Restarting site...' ) );
await startWordPressServer( site );
logger.reportSuccess( __( 'Site restarted' ) );
}
} catch ( error ) {
if ( error instanceof LoggerError ) {
logger.reportError( error );
} else {
const loggerError = new LoggerError( __( 'Failed to start site infrastructure' ), error );
logger.reportError( loggerError );
}
} finally {
disconnect();
}
}

export const registerCommand = ( yargs: StudioArgv ) => {
return yargs.command( {
command: 'set-domain <domain>',
describe: __( 'Set domain for a local site' ),
builder: ( yargs ) => {
return yargs.positional( 'domain', {
type: 'string',
description: __( 'Domain name' ),
demandOption: true,
} );
},
handler: async ( argv ) => {
await runCommand( argv.path, argv.domain );
},
} );
};
92 changes: 92 additions & 0 deletions cli/commands/site/set-https.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { __, _n } from '@wordpress/i18n';
import { arePathsEqual } from 'common/lib/fs-utils';
import { SiteCommandLoggerAction as LoggerAction } from 'common/logger-actions';
import {
getSiteByFolder,
lockAppdata,
readAppdata,
saveAppdata,
unlockAppdata,
} from 'cli/lib/appdata';
import { connect, disconnect } from 'cli/lib/pm2-manager';
import {
isServerRunning,
startWordPressServer,
stopWordPressServer,
} from 'cli/lib/wordpress-server-manager';
import { Logger, LoggerError } from 'cli/logger';
import { StudioArgv } from 'cli/types';

export async function runCommand( sitePath: string, enableHttps: boolean ): Promise< void > {
const logger = new Logger< LoggerAction >();

try {
logger.reportStart( LoggerAction.LOAD_SITES, __( 'Loading site…' ) );
const site = await getSiteByFolder( sitePath, false );
logger.reportSuccess( __( 'Site loaded' ) );

if ( ! site.customDomain ) {
throw new LoggerError( __( 'Site does not have a custom domain.' ) );
}

if ( site.enableHttps === enableHttps ) {
if ( enableHttps ) {
throw new LoggerError( __( 'HTTPS is already enabled for this site.' ) );
} else {
throw new LoggerError( __( 'HTTPS is already disabled for this site.' ) );
}
}

logger.reportStart( LoggerAction.START_DAEMON, __( 'Starting process daemon...' ) );
await connect();
logger.reportSuccess( __( 'Process daemon started' ) );

try {
await lockAppdata();
const appdata = await readAppdata();
const site = appdata.sites.find( ( site ) => arePathsEqual( site.path, sitePath ) );
if ( ! site ) {
throw new LoggerError( __( 'The specified folder is not added to Studio.' ) );
}
site.enableHttps = enableHttps;
await saveAppdata( appdata );
} finally {
await unlockAppdata();
}

const runningProcess = await isServerRunning( site.id );

if ( runningProcess ) {
logger.reportStart( LoggerAction.START_SITE, __( 'Restarting site...' ) );
await stopWordPressServer( site.id );
await startWordPressServer( site );
logger.reportSuccess( __( 'Site restarted' ) );
}
} catch ( error ) {
if ( error instanceof LoggerError ) {
logger.reportError( error );
} else {
const loggerError = new LoggerError( __( 'Failed to start site infrastructure' ), error );
logger.reportError( loggerError );
}
} finally {
disconnect();
}
}

export const registerCommand = ( yargs: StudioArgv ) => {
return yargs.command( {
command: 'set-https <enable>',
describe: __( 'Set HTTPS for a local site' ),
builder: ( yargs ) => {
return yargs.positional( 'enable', {
type: 'boolean',
description: __( 'Enable HTTPS' ),
demandOption: true,
} );
},
handler: async ( argv ) => {
await runCommand( argv.path, argv.enable );
},
} );
};
91 changes: 91 additions & 0 deletions cli/commands/site/set-php-version.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { SupportedPHPVersions } from '@php-wasm/universal';
import { __, _n } from '@wordpress/i18n';
import { arePathsEqual } from 'common/lib/fs-utils';
import { SiteCommandLoggerAction as LoggerAction } from 'common/logger-actions';
import {
getSiteByFolder,
lockAppdata,
readAppdata,
saveAppdata,
unlockAppdata,
} from 'cli/lib/appdata';
import { connect, disconnect } from 'cli/lib/pm2-manager';
import {
isServerRunning,
startWordPressServer,
stopWordPressServer,
} from 'cli/lib/wordpress-server-manager';
import { Logger, LoggerError } from 'cli/logger';
import { StudioArgv } from 'cli/types';

const ALLOWED_PHP_VERSIONS = [ ...SupportedPHPVersions ];

export async function runCommand(
sitePath: string,
phpVersion: ( typeof ALLOWED_PHP_VERSIONS )[ number ]
): Promise< void > {
const logger = new Logger< LoggerAction >();

try {
logger.reportStart( LoggerAction.LOAD_SITES, __( 'Loading site…' ) );
const site = await getSiteByFolder( sitePath, false );
logger.reportSuccess( __( 'Site loaded' ) );

if ( site.phpVersion === phpVersion ) {
throw new LoggerError( __( 'Site is already using the specified PHP version.' ) );
}

try {
await lockAppdata();
const appdata = await readAppdata();
const site = appdata.sites.find( ( site ) => arePathsEqual( site.path, sitePath ) );
if ( ! site ) {
throw new LoggerError( __( 'The specified folder is not added to Studio.' ) );
}
site.phpVersion = phpVersion;
await saveAppdata( appdata );
} finally {
await unlockAppdata();
}

logger.reportStart( LoggerAction.START_DAEMON, __( 'Starting process daemon...' ) );
await connect();
logger.reportSuccess( __( 'Process daemon started' ) );

const runningProcess = await isServerRunning( site.id );

if ( runningProcess ) {
logger.reportStart( LoggerAction.START_SITE, __( 'Restarting site...' ) );
await stopWordPressServer( site.id );
await startWordPressServer( site );
logger.reportSuccess( __( 'Site restarted' ) );
}
} catch ( error ) {
if ( error instanceof LoggerError ) {
logger.reportError( error );
} else {
const loggerError = new LoggerError( __( 'Failed to start site infrastructure' ), error );
logger.reportError( loggerError );
}
} finally {
disconnect();
}
}

export const registerCommand = ( yargs: StudioArgv ) => {
return yargs.command( {
command: 'set-php-version <php-version>',
describe: __( 'Set PHP version for a local site' ),
builder: ( yargs ) => {
return yargs.positional( 'php-version', {
type: 'string',
description: __( 'PHP version' ),
demandOption: true,
choices: ALLOWED_PHP_VERSIONS,
} );
},
handler: async ( argv ) => {
await runCommand( argv.path, argv.phpVersion );
},
} );
};
Loading