Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
becd91f
Add support for Blueprints login with UI fields
bcotrim Feb 3, 2026
b330fe5
Add error handling for user creation and security note for CLI passwords
bcotrim Feb 4, 2026
33e613e
Fix hardcoded admin username in CLI output and site settings UI
bcotrim Feb 4, 2026
cdd1dbd
Add default values and validation for admin credentials in create sit…
bcotrim Feb 4, 2026
d7e07ff
Fix adminUsername not being persisted in toDiskFormat
bcotrim Feb 4, 2026
20ec434
Fix password encoding, i18n, and credential editing guard
bcotrim Feb 4, 2026
9e0cc1a
Add username sanitization and PasswordControl tests
bcotrim Feb 4, 2026
fea6997
Add sanitize_user fallback, username format validation, and Unicode t…
bcotrim Feb 4, 2026
a8330fe
Add credential editing to site settings UI and CLI set command
bcotrim Feb 5, 2026
df06509
Fix add-site test assertions for credential args
bcotrim Feb 5, 2026
55e7f44
Merge branch 'trunk' into bcotrim/stu-829-blueprints-login
bcotrim Feb 5, 2026
a60cc40
Fix hardcoded admin username in site status command
bcotrim Feb 6, 2026
ab2fdc4
Add admin email field to site settings and CLI
bcotrim Feb 8, 2026
ecd224c
Merge branch 'trunk' into bcotrim/stu-829-blueprints-login
bcotrim Feb 11, 2026
c3f6846
copilot feedback
bcotrim Feb 11, 2026
5782dc9
Merge branch 'trunk' into bcotrim/stu-829-blueprints-login
bcotrim Feb 11, 2026
298f79e
Merge branch 'trunk' into bcotrim/stu-829-blueprints-login
bcotrim Feb 12, 2026
5d641eb
Add comment explaining username change creates new user
bcotrim Feb 12, 2026
b41fc38
Merge trunk and make form field labels consistent
bcotrim Feb 13, 2026
aa79075
Fix prettier formatting in create-site-form
bcotrim Feb 13, 2026
e15dcee
Merge trunk and resolve monorepo restructure conflicts
bcotrim Feb 19, 2026
1219f51
Merge remote-tracking branch 'origin/trunk' into bcotrim/stu-829-blue…
bcotrim Feb 20, 2026
e13ace1
Address PR feedback: credential form UX improvements
bcotrim Feb 20, 2026
a4a8363
Merge remote-tracking branch 'origin/trunk' into bcotrim/stu-829-blue…
bcotrim Feb 20, 2026
ef8e1e2
Fix import ordering and prettier formatting
bcotrim Feb 20, 2026
8a64388
Merge branch 'trunk' into bcotrim/stu-829-blueprints-login
bcotrim Feb 23, 2026
1c57a40
Fix credential updates for legacy sites and copySite missing fields
bcotrim Feb 23, 2026
8c43a63
Merge trunk and resolve conflicts with debug log/display feature
bcotrim Feb 23, 2026
8f06c9d
Merge trunk and resolve create command config conflict
bcotrim Feb 26, 2026
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
61 changes: 59 additions & 2 deletions apps/cli/commands/site/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
DEFAULT_WORDPRESS_VERSION,
MINIMUM_WORDPRESS_VERSION,
} from '@studio/common/constants';
import { extractFormValuesFromBlueprint } from '@studio/common/lib/blueprint-settings';
import {
filterUnsupportedBlueprintFeatures,
validateBlueprintData,
Expand All @@ -23,7 +24,12 @@ import {
} from '@studio/common/lib/fs-utils';
import { DEFAULT_LOCALE } from '@studio/common/lib/locale';
import { isOnline } from '@studio/common/lib/network-utils';
import { createPassword } from '@studio/common/lib/passwords';
import {
createPassword,
encodePassword,
validateAdminEmail,
validateAdminUsername,
} from '@studio/common/lib/passwords';
import { portFinder } from '@studio/common/lib/port-finder';
import {
hasDefaultDbBlock,
Expand Down Expand Up @@ -77,6 +83,9 @@ type CreateCommandOptions = {
contents: unknown;
uri: string;
};
adminUsername?: string;
adminPassword?: string;
adminEmail?: string;
noStart: boolean;
skipBrowser: boolean;
skipLogDetails: boolean;
Expand All @@ -101,6 +110,7 @@ export async function runCommand(

let blueprintUri: string | undefined;
let blueprint: Blueprint | undefined;
let blueprintCredentials: { adminUsername?: string; adminPassword?: string } | null = null;

if ( options.blueprint ) {
const validation = await validateBlueprintData( options.blueprint.contents );
Expand All @@ -119,6 +129,15 @@ export async function runCommand(
);
}

// Extract login credentials from blueprint before filtering
const formValues = extractFormValuesFromBlueprint( options.blueprint.contents as Blueprint );
if ( formValues.adminUsername || formValues.adminPassword ) {
blueprintCredentials = {
adminUsername: formValues.adminUsername,
adminPassword: formValues.adminPassword,
};
}

blueprintUri = options.blueprint.uri;
blueprint = filterUnsupportedBlueprintFeatures(
options.blueprint.contents as Record< string, unknown >
Expand Down Expand Up @@ -193,7 +212,26 @@ export async function runCommand(

const siteName = options.name || path.basename( sitePath );
const siteId = options.siteId || crypto.randomUUID();
const adminPassword = createPassword();

// Determine admin credentials: CLI args > Blueprint > defaults
// External passwords need to be encoded; createPassword() already returns encoded
const adminUsername = options.adminUsername || blueprintCredentials?.adminUsername || undefined;
if ( adminUsername ) {
const usernameError = validateAdminUsername( adminUsername );
if ( usernameError ) {
throw new LoggerError( usernameError );
}
}
const adminEmail = options.adminEmail?.trim() || undefined;
if ( adminEmail ) {
const emailError = validateAdminEmail( adminEmail );
if ( emailError ) {
throw new LoggerError( emailError );
}
}

const externalPassword = options.adminPassword || blueprintCredentials?.adminPassword;
const adminPassword = externalPassword ? encodePassword( externalPassword ) : createPassword();

const setupSteps: StepDefinition[] = [];

Expand Down Expand Up @@ -266,7 +304,9 @@ export async function runCommand(
id: siteId,
name: siteName,
path: sitePath,
adminUsername,
adminPassword,
adminEmail,
port,
phpVersion: options.phpVersion,
running: false,
Expand Down Expand Up @@ -488,6 +528,20 @@ export const registerCommand = ( yargs: StudioArgv ) => {
type: 'string',
describe: __( 'Path or URL to Blueprint JSON file' ),
} )
.option( 'admin-username', {
type: 'string',
describe: __( 'Admin username (defaults to "admin")' ),
} )
.option( 'admin-password', {
type: 'string',
describe: __(
'Admin password (auto-generated if not provided). Note: passwords in CLI arguments may be visible in process lists; consider using a Blueprint file for sensitive passwords.'
),
} )
.option( 'admin-email', {
type: 'string',
describe: __( 'Admin email (defaults to "admin@localhost.com")' ),
} )
.option( 'start', {
type: 'boolean',
describe: __( 'Start the site after creation' ),
Expand Down Expand Up @@ -604,6 +658,9 @@ export const registerCommand = ( yargs: StudioArgv ) => {
phpVersion,
customDomain,
enableHttps,
adminUsername: argv.adminUsername,
adminPassword: argv.adminPassword,
adminEmail: argv.adminEmail,
noStart: ! argv.start,
skipBrowser: !! argv.skipBrowser,
skipLogDetails: !! argv.skipLogDetails,
Expand Down
80 changes: 78 additions & 2 deletions apps/cli/commands/site/set.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ import { SupportedPHPVersions } from '@php-wasm/universal';
import { DEFAULT_WORDPRESS_VERSION, MINIMUM_WORDPRESS_VERSION } from '@studio/common/constants';
import { getDomainNameValidationError } from '@studio/common/lib/domains';
import { arePathsEqual } from '@studio/common/lib/fs-utils';
import {
encodePassword,
validateAdminEmail,
validateAdminUsername,
} from '@studio/common/lib/passwords';
import { SITE_EVENTS } from '@studio/common/lib/site-events';
import { siteNeedsRestart } from '@studio/common/lib/site-needs-restart';
import {
Expand Down Expand Up @@ -44,12 +49,27 @@ export interface SetCommandOptions {
php?: string;
wp?: string;
xdebug?: boolean;
adminUsername?: string;
adminPassword?: string;
adminEmail?: string;
debugLog?: boolean;
debugDisplay?: boolean;
}

export async function runCommand( sitePath: string, options: SetCommandOptions ): Promise< void > {
const { name, domain, https, php, wp, xdebug, debugLog, debugDisplay } = options;
const {
name,
domain,
https,
php,
wp,
xdebug,
adminUsername,
adminPassword,
debugLog,
debugDisplay,
} = options;
let { adminEmail } = options;

if (
name === undefined &&
Expand All @@ -58,12 +78,15 @@ export async function runCommand( sitePath: string, options: SetCommandOptions )
php === undefined &&
wp === undefined &&
xdebug === undefined &&
adminUsername === undefined &&
adminPassword === undefined &&
adminEmail === undefined &&
debugLog === undefined &&
debugDisplay === undefined
) {
throw new LoggerError(
__(
'At least one option (--name, --domain, --https, --php, --wp, --xdebug, --debug-log, --debug-display) is required.'
'At least one option (--name, --domain, --https, --php, --wp, --xdebug, --admin-username, --admin-password, --admin-email, --debug-log, --debug-display) is required.'
)
);
}
Expand All @@ -72,6 +95,28 @@ export async function runCommand( sitePath: string, options: SetCommandOptions )
throw new LoggerError( __( 'Site name cannot be empty.' ) );
}

if ( adminUsername !== undefined ) {
const usernameError = validateAdminUsername( adminUsername );
if ( usernameError ) {
throw new LoggerError( usernameError );
}
}

if ( adminPassword !== undefined && ! adminPassword.trim() ) {
throw new LoggerError( __( 'Admin password cannot be empty.' ) );
}

if ( adminEmail !== undefined ) {
if ( ! adminEmail.trim() ) {
adminEmail = undefined;
} else {
const emailError = validateAdminEmail( adminEmail );
if ( emailError ) {
throw new LoggerError( emailError );
}
}
}

try {
logger.reportStart( LoggerAction.LOAD_SITES, __( 'Loading site…' ) );
let site = await getSiteByFolder( sitePath );
Expand Down Expand Up @@ -118,6 +163,11 @@ export async function runCommand( sitePath: string, options: SetCommandOptions )
const phpChanged = php !== undefined && php !== site.phpVersion;
const wpChanged = wp !== undefined;
const xdebugChanged = xdebug !== undefined && xdebug !== site.enableXdebug;
const adminUsernameChanged =
adminUsername !== undefined && adminUsername !== ( site.adminUsername ?? 'admin' );
const adminPasswordChanged = adminPassword !== undefined;
const adminEmailChanged = adminEmail !== undefined && adminEmail !== ( site.adminEmail ?? '' );
const credentialsChanged = adminUsernameChanged || adminPasswordChanged || adminEmailChanged;
const debugLogChanged = debugLog !== undefined && debugLog !== site.enableDebugLog;
const debugDisplayChanged =
debugDisplay !== undefined && debugDisplay !== site.enableDebugDisplay;
Expand All @@ -129,6 +179,7 @@ export async function runCommand( sitePath: string, options: SetCommandOptions )
phpChanged ||
wpChanged ||
xdebugChanged ||
credentialsChanged ||
debugLogChanged ||
debugDisplayChanged;
if ( ! hasChanges ) {
Expand All @@ -143,6 +194,7 @@ export async function runCommand( sitePath: string, options: SetCommandOptions )
phpChanged,
wpChanged,
xdebugChanged,
credentialsChanged,
debugLogChanged,
debugDisplayChanged,
} );
Expand Down Expand Up @@ -171,6 +223,15 @@ export async function runCommand( sitePath: string, options: SetCommandOptions )
if ( xdebugChanged ) {
foundSite.enableXdebug = xdebug;
}
if ( adminUsernameChanged ) {
foundSite.adminUsername = adminUsername!;
}
if ( adminPasswordChanged ) {
foundSite.adminPassword = encodePassword( adminPassword! );
}
if ( adminEmailChanged ) {
foundSite.adminEmail = adminEmail!;
}
if ( debugLogChanged ) {
foundSite.enableDebugLog = debugLog;
}
Expand Down Expand Up @@ -312,6 +373,18 @@ export const registerCommand = ( yargs: StudioArgv ) => {
type: 'boolean',
description: __( 'Enable Xdebug' ),
} )
.option( 'admin-username', {
type: 'string',
description: __( 'Admin username' ),
} )
.option( 'admin-password', {
type: 'string',
description: __( 'Admin password' ),
} )
.option( 'admin-email', {
type: 'string',
description: __( 'Admin email' ),
} )
.option( 'debug-log', {
type: 'boolean',
description: __( 'Enable WP_DEBUG_LOG' ),
Expand All @@ -330,6 +403,9 @@ export const registerCommand = ( yargs: StudioArgv ) => {
php: argv.php,
wp: argv.wp,
xdebug: argv.xdebug,
adminUsername: argv.adminUsername,
adminPassword: argv.adminPassword,
adminEmail: argv.adminEmail,
debugLog: argv.debugLog,
debugDisplay: argv.debugDisplay,
} );
Expand Down
7 changes: 6 additions & 1 deletion apps/cli/commands/site/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,17 @@ export async function runCommand( siteFolder: string, format: 'table' | 'json' )
{ key: __( 'PHP version' ), jsonKey: 'phpVersion', value: site.phpVersion },
{ key: __( 'WP version' ), jsonKey: 'wpVersion', value: wpVersion },
{ key: __( 'Xdebug' ), jsonKey: 'xdebug', value: xdebugStatus },
{ key: __( 'Admin username' ), jsonKey: 'adminUsername', value: 'admin' },
{
key: __( 'Admin username' ),
jsonKey: 'adminUsername',
value: site.adminUsername ?? 'admin',
},
{
key: __( 'Admin password' ),
jsonKey: 'adminPassword',
value: site.adminPassword ? decodePassword( site.adminPassword ) : undefined,
},
{ key: __( 'Admin email' ), jsonKey: 'adminEmail', value: site.adminEmail },
].filter( ( { value, hidden } ) => value && ! hidden );

if ( format === 'table' ) {
Expand Down
Loading