Skip to content
15 changes: 13 additions & 2 deletions cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { registerCommand as registerDeleteCommand } from 'cli/commands/preview/d
import { registerCommand as registerListCommand } from 'cli/commands/preview/list';
import { registerCommand as registerUpdateCommand } from 'cli/commands/preview/update';
import { registerCommand as registerSiteListCommand } from 'cli/commands/site/list';
import { readAppdata } from 'cli/lib/appdata';
import { loadTranslations } from 'cli/lib/i18n';
import { bumpAggregatedUniqueStat } from 'cli/lib/stats';
import { version } from 'cli/package.json';
Expand Down Expand Up @@ -54,8 +55,18 @@ async function main() {
.demandCommand( 1, __( 'You must provide a valid command' ) )
.strict();

if ( process.env.ENABLE_CLI_V2 === 'true' ) {
studioArgv.command( 'site', __( 'Manage local sites' ), ( sitesYargs ) => {
// Check if Studio Sites CLI beta feature is enabled
let isSitesCliEnabled = false;
try {
const appdata = await readAppdata();
isSitesCliEnabled = appdata.betaFeatures?.studioSitesCli ?? false;
} catch ( error ) {
// If we can't read appdata, the feature is not enabled
isSitesCliEnabled = false;
}

if ( isSitesCliEnabled ) {
studioArgv.command( 'site', __( 'Manage local sites (Beta)' ), ( sitesYargs ) => {
sitesYargs.option( 'path', {
hidden: true,
} );
Expand Down
7 changes: 7 additions & 0 deletions cli/lib/appdata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ const newSiteSchema = z
} )
.passthrough();

const betaFeaturesSchema = z
.object( {
studioSitesCli: z.boolean().optional(),
} )
.passthrough();

const userDataSchema = z
.object( {
newSites: z.array( newSiteSchema ).default( () => [] ),
Expand All @@ -47,6 +53,7 @@ const userDataSchema = z
lastBumpStats: z
.record( z.string(), z.record( z.nativeEnum( StatsMetric ), z.number() ) )
.optional(),
betaFeatures: betaFeaturesSchema.optional(),
} )
.passthrough();

Expand Down
5 changes: 5 additions & 0 deletions src/ipc-types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,11 @@ interface FeatureFlags {
enableBlueprints: boolean;
}

// eslint-disable-next-line @typescript-eslint/no-empty-object-type

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems we don't need it. Also we can remove it from FeatureFlags.

Suggested change
// eslint-disable-next-line @typescript-eslint/no-empty-object-type

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wasn't it left here to limit the number of changes we need to make when we remove the last feature flag?

interface BetaFeatures {
studioSitesCli: boolean;
}

interface AppGlobals extends FeatureFlags {
platform: NodeJS.Platform;
appName: string;
Expand Down
53 changes: 53 additions & 0 deletions src/lib/beta-features.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { lockAppdata, unlockAppdata, loadUserData, saveUserData } from 'src/storage/user-data';

export interface BetaFeatureDefinition {
label: string;
key: string;
default: boolean;
description?: string;
}

const BETA_FEATURES_DEFINITION: Record< keyof BetaFeatures, BetaFeatureDefinition > = {
studioSitesCli: {
label: 'Studio Sites CLI',
key: 'studioSitesCli',
default: false,
description: '"studio site" command to manage local sites from terminal',
},
} as const;

export const BETA_FEATURES: Record< keyof BetaFeatures, BetaFeatureDefinition > =
BETA_FEATURES_DEFINITION;

function buildBetaFeatures( userData: BetaFeatures | undefined ): BetaFeatures {
const features: Partial< BetaFeatures > = {};
const keys = Object.keys( BETA_FEATURES );
keys.forEach( ( key ) => {
const featureKey = key as keyof BetaFeatures;
const definition = BETA_FEATURES[ featureKey ];
( features as Record< string, boolean > )[ key ] =
userData?.[ featureKey ] ?? definition.default;
} );
return features as BetaFeatures;
}

export async function getBetaFeatures(): Promise< BetaFeatures > {
const userData = await loadUserData();
return buildBetaFeatures( userData.betaFeatures );
}

export async function updateBetaFeature(
key: keyof BetaFeatures,
value: boolean
): Promise< void > {
try {
await lockAppdata();
const userData = await loadUserData();
const betaFeatures: BetaFeatures = userData.betaFeatures || ( {} as BetaFeatures );
betaFeatures[ key ] = value;
userData.betaFeatures = betaFeatures;
await saveUserData( userData );
} finally {
await unlockAppdata();
}
}
40 changes: 37 additions & 3 deletions src/menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ import { __ } from '@wordpress/i18n';
import { openAboutWindow } from 'src/about-menu/open-about-menu';
import { BUG_REPORT_URL, FEATURE_REQUEST_URL } from 'src/constants';
import { sendIpcEventToRenderer } from 'src/ipc-utils';
import {
BETA_FEATURES,
BetaFeatureDefinition,
getBetaFeatures,
updateBetaFeature,
} from 'src/lib/beta-features';
import {
FEATURE_FLAGS,
FeatureFlagDefinition,
Expand All @@ -30,7 +36,7 @@ export async function setupMenu( config: { needsOnboarding: boolean } ) {
Menu.setApplicationMenu( null );
return;
}
const menu = getAppMenu( mainWindow, config );
const menu = await getAppMenu( mainWindow, config );
if ( process.platform === 'darwin' ) {
Menu.setApplicationMenu( menu );
return;
Expand All @@ -49,11 +55,33 @@ export function removeMenu() {

export async function popupMenu() {
const window = await getMainWindow();
const menu = getAppMenu( window );
const menu = await getAppMenu( window );
menu.popup();
}

function getAppMenu(
async function buildBetaFeaturesMenu(): Promise< MenuItemConstructorOptions[] > {
const currentBetaFeatures = await getBetaFeatures();
return Object.entries< BetaFeatureDefinition >( BETA_FEATURES ).map( ( [ key, definition ] ) => {
// On Windows, use the description as the label for a more compact display
const label =
process.platform === 'win32' && definition.description
? definition.description
: definition.label;

return {
label,
type: 'checkbox' as const,
checked: currentBetaFeatures[ key as keyof BetaFeatures ],
// Only use sublabel on macOS where it displays nicely
sublabel: process.platform === 'darwin' ? definition.description : undefined,
click: async ( menuItem: MenuItem ) => {
await updateBetaFeature( key as keyof BetaFeatures, menuItem.checked );
},
};
} );
}

async function getAppMenu(
mainWindow: BrowserWindow | null,
{ needsOnboarding = false }: { needsOnboarding?: boolean } = {}
) {
Expand Down Expand Up @@ -91,6 +119,8 @@ function getAppMenu(
},
} ) );

const betaFeaturesMenu = await buildBetaFeaturesMenu();

return Menu.buildFromTemplate( [
{
label: app.name, // macOS ignores this name and uses the name from the .plist
Expand Down Expand Up @@ -124,6 +154,10 @@ function getAppMenu(
},
]
: [] ),
{
label: __( 'Beta Features' ),
submenu: betaFeaturesMenu,
},
{ type: 'separator' },
...( process.platform === 'win32'
? []
Expand Down
1 change: 1 addition & 0 deletions src/storage/storage-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export interface UserData {
preferredTerminal?: SupportedTerminal;
preferredEditor?: SupportedEditor;
newSites?: NewSiteDetails[];
betaFeatures?: BetaFeatures;
}

export interface PersistedUserData extends Omit< UserData, 'sites' > {
Expand Down
3 changes: 2 additions & 1 deletion src/storage/user-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,8 @@ type UserDataSafeKeys =
| 'sentryUserId'
| 'lastSeenVersion'
| 'preferredTerminal'
| 'preferredEditor';
| 'preferredEditor'
| 'betaFeatures';

type PartialUserDataWithSafeKeysToUpdate = Partial< Pick< UserData, UserDataSafeKeys > >;

Expand Down