diff --git a/cli/commands/ai/generate.ts b/cli/commands/ai/generate.ts
new file mode 100644
index 000000000..18bfabf86
--- /dev/null
+++ b/cli/commands/ai/generate.ts
@@ -0,0 +1,302 @@
+import { input, select, confirm } from '@inquirer/prompts';
+import { __ } from '@wordpress/i18n';
+import { getAuthToken } from 'cli/lib/appdata';
+import ora from 'ora';
+import chalk from 'chalk';
+import type { StudioArgv } from 'cli/types';
+import { TelexClient } from 'cli/lib/telex-client';
+import { parseArtefactXml, getBlockMetadata } from 'cli/lib/artefact-parser';
+import {
+ installBlockToSite,
+ listStudioSites,
+ studioSiteExists,
+ getPluginActivationUrl,
+} from 'cli/lib/block-installer';
+import { getTelexApiUrl } from 'cli/lib/telex-constants';
+
+interface GenerateOptions {
+ prompt?: string;
+ site?: string;
+}
+
+/**
+ * Run the 'ai generate' command to create a WordPress block using AI
+ */
+export async function runCommand( options: GenerateOptions ): Promise< void > {
+ const spinner = ora();
+
+ try {
+ // 1. Check authentication
+ spinner.start( __( 'Checking authentication...' ) );
+ const authToken = await getAuthToken();
+ if ( ! authToken ) {
+ spinner.fail( __( 'Not authenticated with WordPress.com' ) );
+ console.log(
+ chalk.yellow( '\nPlease run:' ),
+ chalk.cyan( 'studio auth login' )
+ );
+ return;
+ }
+ spinner.succeed(
+ __( 'Authenticated as %s', chalk.cyan( authToken.displayName ) )
+ );
+
+ // 2. Get prompt from user or flag
+ const prompt =
+ options.prompt ||
+ ( await input( {
+ message: __( 'Describe the block you want to create:' ),
+ validate: ( value ) =>
+ value.trim().length > 0 || __( 'Please enter a prompt' ),
+ } ) );
+
+ console.log( chalk.dim( `\nPrompt: "${ prompt }"\n` ) );
+
+ // 3. Initialize Telex client
+ const telexApiUrl = getTelexApiUrl();
+ const telex = new TelexClient( telexApiUrl, authToken.accessToken );
+
+ // 4. Generate block
+ spinner.start( __( 'Connecting to Telex AI...' ) );
+
+ let chatText = '';
+ let currentLine = '';
+
+ try {
+ const result = await telex.generateBlock( prompt, {
+ onChunk: ( text ) => {
+ chatText += text;
+ currentLine += text;
+
+ // Update spinner with last line of AI response
+ if ( text.includes( '\n' ) ) {
+ const lines = currentLine.split( '\n' );
+ currentLine = lines[ lines.length - 1 ];
+ }
+
+ const displayText =
+ currentLine.length > 60
+ ? currentLine.slice( -60 )
+ : currentLine;
+ spinner.text = chalk.cyan( `AI: ${ displayText.trim() }` );
+ },
+ onArtefact: () => {
+ spinner.text = __( 'Receiving block files...' );
+ },
+ } );
+
+ spinner.succeed( __( 'Block generated successfully!' ) );
+
+ // Show AI explanation
+ if ( chatText ) {
+ console.log( chalk.dim( '\n' + '─'.repeat( 60 ) ) );
+ console.log( chalk.cyan( '\nAI Response:\n' ) );
+ console.log( chatText.trim() );
+ console.log( chalk.dim( '\n' + '─'.repeat( 60 ) + '\n' ) );
+ }
+
+ // 5. Parse artefact
+ const artefact = parseArtefactXml( result.artefact );
+ const metadata = getBlockMetadata( artefact );
+
+ // Show block info
+ console.log( chalk.bold( '\nGenerated Block:' ) );
+ console.log( chalk.gray( ' Name: ' ), chalk.white( artefact.name ) );
+ console.log( chalk.gray( ' Slug: ' ), chalk.white( artefact.slug ) );
+ console.log(
+ chalk.gray( ' Files: ' ),
+ chalk.white( artefact.files.length )
+ );
+
+ if ( metadata?.title ) {
+ console.log(
+ chalk.gray( ' Title: ' ),
+ chalk.white( metadata.title )
+ );
+ }
+
+ // 6. Ask what to do with the block
+ const action = await select( {
+ message: __( 'What would you like to do?' ),
+ choices: [
+ {
+ value: 'install',
+ name: __( 'Install to local Studio site' ),
+ },
+ {
+ value: 'preview',
+ name: __( 'Open in Telex web editor' ),
+ },
+ {
+ value: 'exit',
+ name: __( 'Exit (block saved in Telex)' ),
+ },
+ ],
+ } );
+
+ // 7. Handle action
+ if ( action === 'install' ) {
+ await handleInstall( artefact, options.site, spinner );
+ } else if ( action === 'preview' ) {
+ const url = telex.getProjectUrl( result.epid );
+ console.log(
+ chalk.green( '\n✓' ),
+ __( 'Open in your browser:' ),
+ chalk.cyan( url )
+ );
+ } else {
+ console.log(
+ chalk.green( '\n✓' ),
+ __( 'Block saved to your Telex account' )
+ );
+ }
+ } catch ( error ) {
+ spinner.fail( __( 'Block generation failed' ) );
+
+ if ( error instanceof Error ) {
+ console.error( chalk.red( '\nError:' ), error.message );
+ }
+
+ throw error;
+ }
+ } catch ( error ) {
+ if ( spinner.isSpinning ) {
+ spinner.fail();
+ }
+
+ if ( error instanceof Error && error.message !== 'User force closed the prompt' ) {
+ // Don't show error if user pressed Ctrl+C
+ console.error( chalk.red( '\nCommand failed:' ), error.message );
+ }
+
+ process.exit( 1 );
+ }
+}
+
+/**
+ * Handle block installation to Studio site
+ */
+async function handleInstall(
+ artefact: ReturnType< typeof parseArtefactXml >,
+ siteName: string | undefined,
+ spinner: ReturnType< typeof ora >
+): Promise< void > {
+ // Get or select site
+ let selectedSite = siteName;
+
+ if ( ! selectedSite ) {
+ const sites = await listStudioSites();
+
+ if ( sites.length === 0 ) {
+ console.log(
+ chalk.yellow( '\n⚠ No Studio sites found' )
+ );
+ console.log(
+ chalk.gray( 'Create a site with:' ),
+ chalk.cyan( 'studio site create' )
+ );
+ return;
+ }
+
+ selectedSite = await select( {
+ message: __( 'Select a Studio site:' ),
+ choices: sites.map( ( site ) => ( {
+ value: site,
+ name: site,
+ } ) ),
+ } );
+ }
+
+ // Verify site exists
+ if ( ! studioSiteExists( selectedSite ) ) {
+ console.error(
+ chalk.red( `\n✗ Site '${ selectedSite }' not found` )
+ );
+ console.log(
+ chalk.gray( 'Available sites:' ),
+ ( await listStudioSites() ).join( ', ' )
+ );
+ return;
+ }
+
+ // Check if plugin already exists
+ const overwrite = await confirm( {
+ message: __(
+ 'Install "%s" to %s?',
+ artefact.name,
+ selectedSite
+ ),
+ default: true,
+ } );
+
+ if ( ! overwrite ) {
+ console.log( chalk.yellow( '\nInstallation cancelled' ) );
+ return;
+ }
+
+ // Install
+ spinner.start( __( 'Installing block to %s...', selectedSite ) );
+
+ try {
+ await installBlockToSite( selectedSite, artefact );
+ spinner.succeed( __( 'Block installed to %s', chalk.cyan( selectedSite ) ) );
+
+ // Show next steps
+ const pluginUrl = getPluginActivationUrl( selectedSite, artefact.slug );
+
+ console.log( chalk.bold( '\n📦 Next Steps:' ) );
+ console.log( chalk.gray( ' 1.' ), 'Visit', chalk.cyan( pluginUrl ) );
+ console.log(
+ chalk.gray( ' 2.' ),
+ 'Activate',
+ chalk.cyan( `"${ artefact.name }"` )
+ );
+ console.log(
+ chalk.gray( ' 3.' ),
+ 'Create a post and add the block'
+ );
+ } catch ( error ) {
+ spinner.fail( __( 'Installation failed' ) );
+ throw error;
+ }
+}
+
+/**
+ * Register the 'ai generate' command with Yargs
+ */
+export const registerCommand = ( yargs: StudioArgv ) => {
+ return yargs.command( {
+ command: 'generate [prompt]',
+ describe: __( 'Generate a WordPress block using AI' ),
+ builder: ( yargs ) => {
+ return yargs
+ .positional( 'prompt', {
+ type: 'string',
+ describe: __( 'Describe the block you want to create' ),
+ } )
+ .option( 'site', {
+ type: 'string',
+ describe: __( 'Studio site to install the block to' ),
+ alias: 's',
+ } )
+ .example(
+ '$0 ai generate',
+ __( 'Interactively create a block' )
+ )
+ .example(
+ '$0 ai generate "testimonial carousel"',
+ __( 'Create a block from prompt' )
+ )
+ .example(
+ '$0 ai generate "hero section" --site mysite',
+ __( 'Create and install to site' )
+ );
+ },
+ handler: async ( argv ) => {
+ await runCommand( {
+ prompt: argv.prompt,
+ site: argv.site,
+ } );
+ },
+ } );
+};
diff --git a/cli/commands/telex/block.ts b/cli/commands/telex/block.ts
new file mode 100644
index 000000000..31f586739
--- /dev/null
+++ b/cli/commands/telex/block.ts
@@ -0,0 +1,119 @@
+import { input } from '@inquirer/prompts';
+import { __ } from '@wordpress/i18n';
+import chalk from 'chalk';
+import type { StudioArgv } from 'cli/types';
+import { runTelexCommand } from 'cli/lib/telex-command-utils';
+
+interface BlockOptions {
+ prompt?: string;
+}
+
+/**
+ * Run the 'telex block' command to create a WordPress block using AI
+ */
+export async function runCommand( options: BlockOptions, sitePath: string ): Promise< void > {
+ // Get prompt from user or flag
+ const prompt =
+ options.prompt ||
+ ( await input( {
+ message: __( 'Describe the block you want to create:' ),
+ validate: ( value ) =>
+ value.trim().length > 0 || __( 'Please enter a prompt' ),
+ } ) );
+
+ console.log( chalk.dim( `\nPrompt: "${ prompt }"\n` ) );
+
+ // Use shared command flow
+ await runTelexCommand( options, sitePath, async ( telex, spinner ) => {
+ // Generate block with AI
+ spinner.start( __( 'Connecting to Telex AI...' ) );
+
+ let chatText = '';
+ let currentLine = '';
+
+ const result = await telex.generateBlock( prompt, {
+ onChunk: ( text ) => {
+ chatText += text;
+ currentLine += text;
+
+ // Update spinner with last line of AI response
+ if ( text.includes( '\n' ) ) {
+ const lines = currentLine.split( '\n' );
+ currentLine = lines[ lines.length - 1 ];
+ }
+
+ const displayText =
+ currentLine.length > 60
+ ? currentLine.slice( -60 )
+ : currentLine;
+ spinner.text = chalk.cyan( `AI: ${ displayText.trim() }` );
+ },
+ onArtefact: () => {
+ spinner.text = __( 'Receiving block files...' );
+ },
+ } );
+
+ spinner.succeed( __( 'Block generated successfully!' ) );
+
+ // Show AI explanation
+ if ( chatText ) {
+ console.log( chalk.dim( '\n' + '─'.repeat( 60 ) ) );
+ console.log( chalk.cyan( '\nAI Response:\n' ) );
+ console.log( chatText.trim() );
+ console.log( chalk.dim( '\n' + '─'.repeat( 60 ) + '\n' ) );
+ }
+
+ // Show Telex UI URL
+ const telexUrl = telex.getProjectUrl( result.epid );
+ console.log(
+ chalk.gray( '\n Telex: ' ),
+ chalk.cyan( telexUrl )
+ );
+
+ // Wait for build and fetch complete block with built files
+ spinner.start( __( 'Waiting for build to complete...' ) );
+ const artefact = await telex.fetchBlock( result.epid, undefined, ( attempt, max ) => {
+ spinner.text = __( 'Waiting for build to complete...' ) + ` (${ attempt }/${ max })`;
+ } );
+ spinner.succeed( __( 'Build complete! Block ready for installation.' ) );
+
+ return artefact;
+ } );
+}
+
+/**
+ * Register the 'telex block' command with Yargs
+ */
+export const registerCommand = ( yargs: StudioArgv ) => {
+ return yargs.command( {
+ command: 'block [prompt]',
+ describe: __( 'Generate a WordPress block using AI and install it' ),
+ builder: ( yargs ) => {
+ return yargs
+ .positional( 'prompt', {
+ type: 'string',
+ describe: __( 'Describe the block you want to create' ),
+ } )
+ .example(
+ '$0 telex block',
+ __( 'Interactively create a block' )
+ )
+ .example(
+ '$0 telex block "testimonial carousel"',
+ __( 'Create a testimonial carousel block' )
+ )
+ .example(
+ '$0 telex block "hero section" --path ~/sites/mysite',
+ __( 'Create and install to specific site' )
+ );
+ },
+ handler: async ( argv ) => {
+ await runCommand(
+ {
+ prompt: argv.prompt,
+ },
+ argv.path
+ );
+ },
+ } );
+};
diff --git a/cli/commands/telex/install.ts b/cli/commands/telex/install.ts
new file mode 100644
index 000000000..cb1b7c907
--- /dev/null
+++ b/cli/commands/telex/install.ts
@@ -0,0 +1,91 @@
+import { input } from '@inquirer/prompts';
+import { __ } from '@wordpress/i18n';
+import chalk from 'chalk';
+import type { StudioArgv } from 'cli/types';
+import { runTelexCommand } from 'cli/lib/telex-command-utils';
+
+interface InstallOptions {
+ projectId?: string;
+}
+
+/**
+ * Extract EPID from Telex URL or return as-is if already an EPID
+ * Examples:
+ * - https://telex.automattic.ai/projects/v1.abc123 → v1.abc123
+ * - v1.abc123 → v1.abc123
+ */
+function extractEpid( input: string ): string {
+ const match = input.match( /\/projects\/([^\/\?#]+)/ );
+ return match ? match[ 1 ] : input;
+}
+
+/**
+ * Run the 'telex install' command to install a block from Telex
+ */
+export async function runCommand( options: InstallOptions, sitePath: string ): Promise< void > {
+ // Get project ID from user or flag
+ const projectInput =
+ options.projectId ||
+ ( await input( {
+ message: __( 'Enter Telex project URL or ID:' ),
+ validate: ( value ) =>
+ value.trim().length > 0 || __( 'Please enter a project URL or ID' ),
+ } ) );
+
+ const epid = extractEpid( projectInput );
+ console.log( chalk.dim( `\nProject ID: ${ epid }\n` ) );
+
+ // Use shared command flow
+ await runTelexCommand( options, sitePath, async ( telex, spinner ) => {
+ // Fetch block artefact
+ spinner.start( __( 'Fetching block from Telex...' ) );
+ const artefact = await telex.fetchBlock( epid );
+ spinner.succeed( __( 'Block fetched successfully!' ) );
+
+ // Show Telex UI URL
+ const telexUrl = telex.getProjectUrl( epid );
+ console.log(
+ chalk.gray( ' Telex: ' ),
+ chalk.cyan( telexUrl )
+ );
+
+ return artefact;
+ } );
+}
+
+/**
+ * Register the 'telex install' command with Yargs
+ */
+export const registerCommand = ( yargs: StudioArgv ) => {
+ return yargs.command( {
+ command: 'install [project-id]',
+ describe: __( 'Install a block from Telex to your local site' ),
+ builder: ( yargs ) => {
+ return yargs
+ .positional( 'project-id', {
+ type: 'string',
+ describe: __( 'Telex project URL or ID' ),
+ } )
+ .example(
+ '$0 telex install',
+ __( 'Interactively install a block' )
+ )
+ .example(
+ '$0 telex install v1.abc123',
+ __( 'Install block by ID' )
+ )
+ .example(
+ '$0 telex install https://telex.automattic.ai/projects/v1.abc123',
+ __( 'Install block by URL' )
+ );
+ },
+ handler: async ( argv ) => {
+ await runCommand(
+ {
+ projectId: argv.projectId,
+ },
+ argv.path
+ );
+ },
+ } );
+};
diff --git a/cli/index.ts b/cli/index.ts
index d544db03b..f1938e812 100644
--- a/cli/index.ts
+++ b/cli/index.ts
@@ -6,6 +6,8 @@ import yargs from 'yargs';
import { registerCommand as registerAuthLoginCommand } from 'cli/commands/auth/login';
import { registerCommand as registerAuthLogoutCommand } from 'cli/commands/auth/logout';
import { registerCommand as registerAuthStatusCommand } from 'cli/commands/auth/status';
+import { registerCommand as registerTelexBlockCommand } from 'cli/commands/telex/block';
+import { registerCommand as registerTelexInstallCommand } from 'cli/commands/telex/install';
import { registerCommand as registerCreateCommand } from 'cli/commands/preview/create';
import { registerCommand as registerDeleteCommand } from 'cli/commands/preview/delete';
import { registerCommand as registerListCommand } from 'cli/commands/preview/list';
@@ -67,6 +69,11 @@ async function main() {
registerAuthStatusCommand( authYargs );
authYargs.version( false ).demandCommand( 1, __( 'You must provide a valid auth command' ) );
} )
+ .command( 'telex', __( 'AI-assisted block development with Telex' ), ( telexYargs ) => {
+ registerTelexBlockCommand( telexYargs );
+ registerTelexInstallCommand( telexYargs );
+ telexYargs.demandCommand( 1, __( 'You must provide a valid telex command' ) );
+ } )
.command( 'preview', __( 'Manage preview sites' ), ( previewYargs ) => {
registerCreateCommand( previewYargs );
registerListCommand( previewYargs );
diff --git a/cli/lib/artefact-parser.ts b/cli/lib/artefact-parser.ts
new file mode 100644
index 000000000..23b22f04c
--- /dev/null
+++ b/cli/lib/artefact-parser.ts
@@ -0,0 +1,169 @@
+import { DOMParser } from '@xmldom/xmldom';
+
+/**
+ * Parsed artefact file
+ */
+export interface ArtefactFile {
+ path: string; // Relative path (e.g., 'src/index.js')
+ content: string; // File contents
+ description?: string; // Optional description
+}
+
+/**
+ * Parsed artefact data
+ */
+export interface Artefact {
+ name: string; // Block display name (e.g., 'Confetti Button')
+ slug: string; // Machine-readable slug (e.g., 'confetti-button')
+ type: string; // Always 'code-package'
+ schemaVersion: string; // Schema version (e.g., '2')
+ files: ArtefactFile[]; // All files in the block
+}
+
+/**
+ * Parse Telex artefact XML into structured data.
+ *
+ * Artefacts are XML files that contain all block files and metadata
+ * as a single source of truth for WordPress Gutenberg blocks.
+ *
+ * Example:
+ * ```xml
+ *
+ *
+ * Block registration
+ *
+ *
+ *
+ * ```
+ *
+ * @param xml - Artefact XML string
+ * @returns Parsed artefact data
+ * @throws Error if XML is invalid or malformed
+ */
+export function parseArtefactXml( xml: string ): Artefact {
+ try {
+ // Trim whitespace first
+ xml = xml.trim();
+
+ // Extract only the artefact XML (from to )
+ // to ignore any trailing chat text or extra content
+ const artefactStart = xml.indexOf( '' );
+
+ if ( artefactStart === -1 || artefactEnd === -1 ) {
+ throw new Error( 'Missing tags in XML' );
+ }
+
+ const cleanXml = xml.substring( artefactStart, artefactEnd + ''.length ).trim();
+
+ const parser = new DOMParser();
+ const doc = parser.parseFromString( cleanXml, 'text/xml' );
+
+ // Check for parser errors
+ const parseError = doc.getElementsByTagName( 'parsererror' );
+ if ( parseError.length > 0 ) {
+ throw new Error( `XML parsing error: ${ parseError[ 0 ].textContent }` );
+ }
+
+ // Get root artefact element
+ const artefactEl = doc.getElementsByTagName( 'artefact' )[ 0 ];
+ if ( ! artefactEl ) {
+ throw new Error( 'Missing root element' );
+ }
+
+ // Extract attributes
+ const name = artefactEl.getAttribute( 'name' );
+ const slug = artefactEl.getAttribute( 'slug' );
+ const type = artefactEl.getAttribute( 'type' );
+ const schemaVersion = artefactEl.getAttribute( 'schemaVersion' );
+
+ if ( ! name || ! slug ) {
+ throw new Error( 'Missing required attributes: name and slug' );
+ }
+
+ // Parse all elements
+ const fileElements = artefactEl.getElementsByTagName( 'file' );
+ const files: ArtefactFile[] = [];
+
+ for ( let i = 0; i < fileElements.length; i++ ) {
+ const fileEl = fileElements[ i ];
+ const path = fileEl.getAttribute( 'path' );
+
+ if ( ! path ) {
+ console.warn( `File element ${ i } missing path attribute, skipping` );
+ continue;
+ }
+
+ // Get description (optional)
+ const descriptionEl = fileEl.getElementsByTagName( 'description' )[ 0 ];
+ const description = descriptionEl?.textContent?.trim() || undefined;
+
+ // Get content (required)
+ const contentEl = fileEl.getElementsByTagName( 'content' )[ 0 ];
+ const content = contentEl?.textContent || '';
+
+ files.push( {
+ path,
+ content,
+ description,
+ } );
+ }
+
+ return {
+ name,
+ slug,
+ type: type || 'code-package',
+ schemaVersion: schemaVersion || '2',
+ files,
+ };
+ } catch ( error ) {
+ if ( error instanceof Error ) {
+ throw new Error( `Failed to parse artefact XML: ${ error.message }` );
+ }
+ throw error;
+ }
+}
+
+/**
+ * Get a specific file from an artefact by path
+ *
+ * @param artefact - Parsed artefact
+ * @param filePath - File path to search for
+ * @returns File content or null if not found
+ */
+export function getArtefactFile(
+ artefact: Artefact,
+ filePath: string
+): string | null {
+ const file = artefact.files.find( ( f ) => f.path === filePath );
+ return file?.content || null;
+}
+
+/**
+ * Get the main plugin file path for an artefact
+ *
+ * @param artefact - Parsed artefact
+ * @returns Main plugin file path (e.g., 'my-block.php')
+ */
+export function getMainPluginFile( artefact: Artefact ): string {
+ return `${ artefact.slug }.php`;
+}
+
+/**
+ * Extract block metadata from block.json file
+ *
+ * @param artefact - Parsed artefact
+ * @returns Parsed block.json or null if not found/invalid
+ */
+export function getBlockMetadata( artefact: Artefact ): Record< string, unknown > | null {
+ const blockJsonContent = getArtefactFile( artefact, 'src/block.json' );
+ if ( ! blockJsonContent ) {
+ return null;
+ }
+
+ try {
+ return JSON.parse( blockJsonContent );
+ } catch {
+ return null;
+ }
+}
diff --git a/cli/lib/block-installer.ts b/cli/lib/block-installer.ts
new file mode 100644
index 000000000..29076ed23
--- /dev/null
+++ b/cli/lib/block-installer.ts
@@ -0,0 +1,184 @@
+import { writeFile, mkdir, readdir } from 'fs/promises';
+import { join, dirname } from 'path';
+import { homedir } from 'os';
+import { existsSync } from 'fs';
+import type { Artefact } from './artefact-parser';
+
+/**
+ * Get the path to a Studio site's directory
+ *
+ * @param siteName - Name of the Studio site (e.g., 'mysite')
+ * @returns Absolute path to site directory
+ */
+export function getStudioSitePath( siteName: string ): string {
+ // Studio sites are stored in ~/Studio/sites/{siteName}
+ return join( homedir(), 'Studio', 'sites', siteName );
+}
+
+/**
+ * Check if a Studio site exists
+ *
+ * @param siteName - Name of the Studio site
+ * @returns true if site exists
+ */
+export function studioSiteExists( siteName: string ): boolean {
+ const sitePath = getStudioSitePath( siteName );
+ return existsSync( join( sitePath, 'wp-config.php' ) );
+}
+
+/**
+ * List all Studio sites
+ *
+ * @returns Array of site names
+ */
+export async function listStudioSites(): Promise< string[] > {
+ const studiosPath = join( homedir(), 'Studio', 'sites' );
+
+ if ( ! existsSync( studiosPath ) ) {
+ return [];
+ }
+
+ try {
+ const entries = await readdir( studiosPath, { withFileTypes: true } );
+ const sites: string[] = [];
+
+ for ( const entry of entries ) {
+ if ( entry.isDirectory() ) {
+ const wpConfigPath = join( studiosPath, entry.name, 'wp-config.php' );
+ if ( existsSync( wpConfigPath ) ) {
+ sites.push( entry.name );
+ }
+ }
+ }
+
+ return sites;
+ } catch ( error ) {
+ console.warn( 'Failed to list Studio sites:', error );
+ return [];
+ }
+}
+
+/**
+ * Install a block artefact to a site's plugins directory by path.
+ *
+ * Creates the plugin directory and writes all files from the artefact.
+ * The block will appear in WordPress plugins but needs to be activated manually.
+ *
+ * @param sitePath - Full path to the WordPress site directory
+ * @param artefact - Parsed artefact containing block files
+ * @throws Error if site doesn't exist or installation fails
+ */
+export async function installBlockToSitePath(
+ sitePath: string,
+ artefact: Artefact
+): Promise< void > {
+ // Verify site exists
+ const wpConfigPath = join( sitePath, 'wp-config.php' );
+ if ( ! existsSync( wpConfigPath ) ) {
+ throw new Error(
+ `WordPress installation not found at '${ sitePath }'. Missing wp-config.php.`
+ );
+ }
+
+ // Plugin will be installed to wp-content/plugins/{slug}
+ const pluginPath = join( sitePath, 'wp-content', 'plugins', artefact.slug );
+
+ // Create plugin directory
+ await mkdir( pluginPath, { recursive: true } );
+
+ // Write all files from artefact
+ let filesWritten = 0;
+ for ( const file of artefact.files ) {
+ const filePath = join( pluginPath, file.path );
+ const fileDir = dirname( filePath );
+
+ // Ensure directory exists
+ await mkdir( fileDir, { recursive: true } );
+
+ // Write file
+ await writeFile( filePath, file.content, 'utf-8' );
+ filesWritten++;
+ }
+
+ if ( filesWritten === 0 ) {
+ throw new Error( 'No files were written - artefact may be empty' );
+ }
+}
+
+/**
+ * Install a block artefact to a Studio site's plugins directory by name.
+ *
+ * This is a convenience wrapper around installBlockToSitePath() for sites
+ * in the default Studio location (~/Studio/sites/).
+ *
+ * @param siteName - Name of the Studio site (e.g., 'mysite')
+ * @param artefact - Parsed artefact containing block files
+ * @throws Error if site doesn't exist or installation fails
+ */
+export async function installBlockToSite(
+ siteName: string,
+ artefact: Artefact
+): Promise< void > {
+ const sitePath = getStudioSitePath( siteName );
+
+ // Verify site exists
+ if ( ! studioSiteExists( siteName ) ) {
+ throw new Error(
+ `Studio site '${ siteName }' not found. Run 'studio site list' to see available sites.`
+ );
+ }
+
+ return installBlockToSitePath( sitePath, artefact );
+}
+
+/**
+ * Get the WordPress plugin activation URL for a block
+ *
+ * @param siteName - Name of the Studio site
+ * @param pluginSlug - Plugin slug (e.g., 'my-block')
+ * @returns URL to WordPress plugins page
+ */
+export function getPluginActivationUrl( siteName: string, pluginSlug: string ): string {
+ // Studio sites typically use {siteName}.local domain
+ return `http://${ siteName }.local/wp-admin/plugins.php`;
+}
+
+/**
+ * Install options for blocks
+ */
+export interface InstallOptions {
+ /** Overwrite existing plugin if it exists (default: false) */
+ overwrite?: boolean;
+ /** Verbose output (default: false) */
+ verbose?: boolean;
+}
+
+/**
+ * Install a block with options
+ *
+ * @param siteName - Name of the Studio site
+ * @param artefact - Parsed artefact
+ * @param options - Install options
+ */
+export async function installBlock(
+ siteName: string,
+ artefact: Artefact,
+ options: InstallOptions = {}
+): Promise< void > {
+ const sitePath = getStudioSitePath( siteName );
+ const pluginPath = join( sitePath, 'wp-content', 'plugins', artefact.slug );
+
+ // Check if plugin already exists
+ if ( existsSync( pluginPath ) && ! options.overwrite ) {
+ throw new Error(
+ `Plugin '${ artefact.slug }' already exists. Use --overwrite to replace it.`
+ );
+ }
+
+ // Install the block
+ await installBlockToSite( siteName, artefact );
+
+ if ( options.verbose ) {
+ console.log( `Installed ${ artefact.files.length } files to ${ pluginPath }` );
+ }
+}
diff --git a/cli/lib/telex-client.ts b/cli/lib/telex-client.ts
new file mode 100644
index 000000000..da14f4ec8
--- /dev/null
+++ b/cli/lib/telex-client.ts
@@ -0,0 +1,532 @@
+import { EventEmitter } from 'events';
+import { DOMParser } from '@xmldom/xmldom';
+import { TELEX_DEFAULTS } from 'cli/lib/telex-constants';
+
+/**
+ * Artefact file
+ */
+export interface ArtefactFile {
+ path: string; // Relative path (e.g., 'src/index.js' or 'build/index.js')
+ content: string; // File contents
+ description?: string; // Optional description
+}
+
+/**
+ * Artefact data
+ */
+export interface Artefact {
+ name: string; // Block display name
+ slug: string; // Machine-readable slug
+ type: string; // Always 'code-package'
+ schemaVersion: string; // Schema version (e.g., '2')
+ files: ArtefactFile[]; // All files in the block
+}
+
+/**
+ * Block metadata from block.json
+ */
+export interface BlockMetadata {
+ title?: string;
+ name?: string;
+ description?: string;
+ category?: string;
+ supports?: Record< string, unknown >;
+}
+
+/**
+ * Telex AI generation result
+ */
+export interface TelexGenerateResult {
+ artefact: string; // Full artefact XML
+ epid: string; // Encoded project ID
+ chatText: string; // AI explanation text
+}
+
+/**
+ * Options for block generation
+ */
+export interface TelexGenerateOptions {
+ onChunk?: ( text: string ) => void; // Called for each chat text chunk
+ onArtefact?: () => void; // Called when artefact marker detected
+ onArtefactChunk?: ( xml: string ) => void; // Called for each XML chunk
+}
+
+/**
+ * Telex API client for AI-powered WordPress block generation.
+ *
+ * Uses WordPress.com OAuth tokens for authentication (no separate login needed).
+ * Compatible with Studio CLI authentication.
+ */
+export class TelexClient extends EventEmitter {
+ private apiUrl: string;
+ private wpcomToken: string;
+
+ /**
+ * Create a new Telex API client
+ *
+ * @param apiUrl - Base URL of Telex API (e.g., 'https://telex.automattic.ai/api')
+ * @param wpcomToken - WordPress.com OAuth access token
+ */
+ constructor( apiUrl: string, wpcomToken: string ) {
+ super();
+ this.apiUrl = apiUrl.replace( /\/$/, '' ); // Remove trailing slash
+ this.wpcomToken = wpcomToken;
+ }
+
+ /**
+ * Generate a WordPress block from a natural language prompt.
+ *
+ * Uses Claude AI to generate a complete block with all files (PHP, JS, CSS, etc.)
+ * Returns streaming response with real-time chat updates.
+ *
+ * @param prompt - Natural language description of the block to generate
+ * @param options - Optional callbacks for streaming updates
+ * @returns Promise - Generated artefact and metadata
+ */
+ async generateBlock(
+ prompt: string,
+ options: TelexGenerateOptions = {}
+ ): Promise< TelexGenerateResult > {
+ const response = await fetch( `${ this.apiUrl }/assistant`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${ this.wpcomToken }`,
+ },
+ body: JSON.stringify( {
+ prompt,
+ mode: 'chat',
+ stream: true,
+ // Note: No euid/epid needed - Bearer token provides identity
+ } ),
+ } );
+
+ if ( ! response.ok ) {
+ const errorText = await response.text();
+ throw new Error(
+ `Telex API error (${ response.status }): ${ errorText.slice( 0, 200 ) }`
+ );
+ }
+
+ // Parse Server-Sent Events (SSE) stream
+ return this.parseStreamResponse( response, options );
+ }
+
+ /**
+ * Update an existing block with additional instructions.
+ *
+ * @param epid - Encoded project ID of existing block
+ * @param prompt - Instructions for updating the block
+ * @param options - Optional callbacks for streaming updates
+ * @returns Promise - Updated artefact and metadata
+ */
+ async updateBlock(
+ epid: string,
+ prompt: string,
+ options: TelexGenerateOptions = {}
+ ): Promise< TelexGenerateResult > {
+ const response = await fetch( `${ this.apiUrl }/assistant`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${ this.wpcomToken }`,
+ },
+ body: JSON.stringify( {
+ prompt,
+ mode: 'chat',
+ stream: true,
+ epid, // Provide existing project ID
+ } ),
+ } );
+
+ if ( ! response.ok ) {
+ const errorText = await response.text();
+ throw new Error(
+ `Telex API error (${ response.status }): ${ errorText.slice( 0, 200 ) }`
+ );
+ }
+
+ return this.parseStreamResponse( response, options );
+ }
+
+ /**
+ * Parse SSE stream response from Telex API
+ */
+ private async parseStreamResponse(
+ response: Response,
+ options: TelexGenerateOptions
+ ): Promise< TelexGenerateResult > {
+ const reader = response.body?.getReader();
+ if ( ! reader ) {
+ throw new Error( 'Response body is not readable' );
+ }
+
+ const decoder = new TextDecoder();
+ let buffer = '';
+ let chatText = '';
+ let artefactXml = '';
+ let epid = '';
+ let inArtefact = false;
+ let error: string | null = null;
+ let currentEvent = '';
+
+ try {
+ while ( true ) {
+ const { done, value } = await reader.read();
+ if ( done ) break;
+
+ buffer += decoder.decode( value, { stream: true } );
+ const lines = buffer.split( '\n' );
+ buffer = lines.pop() || ''; // Keep incomplete line in buffer
+
+ for ( const line of lines ) {
+ // Track event type from "event:" lines
+ if ( line.startsWith( 'event: ' ) ) {
+ currentEvent = line.slice( 7 ).trim();
+ continue;
+ }
+
+ if ( ! line.startsWith( 'data: ' ) ) continue;
+
+ const data = line.slice( 6 ).trim();
+ if ( data === '[DONE]' ) continue;
+
+
+ try {
+ const eventData = JSON.parse( data );
+
+ switch ( currentEvent ) {
+ case 'chunk':
+ // Chat text streaming
+ const chunkContent = eventData.content || '';
+ chatText += chunkContent;
+ options.onChunk?.( chunkContent );
+ break;
+
+ case 'new_artefact':
+ // Artefact marker detected
+ inArtefact = true;
+ options.onArtefact?.();
+ break;
+
+ case 'artefact_content':
+ // Artefact XML streaming
+ const xmlContent = eventData.content || '';
+ artefactXml += xmlContent;
+ options.onArtefactChunk?.( xmlContent );
+ break;
+
+ case 'artefact_ready':
+ // Generation complete
+ epid = eventData.epid || '';
+ break;
+
+ case 'error':
+ // Error occurred
+ error = eventData.message || 'Unknown error';
+ break;
+
+ case 'retry':
+ // Retry attempt (informational)
+ break;
+
+ case 'end':
+ // End of stream
+ break;
+ }
+ } catch ( e ) {
+ // Ignore JSON parse errors for malformed events
+ console.warn( 'Failed to parse SSE event:', data.slice( 0, 100 ) );
+ }
+ }
+ }
+ } finally {
+ reader.releaseLock();
+ }
+
+ // Check for errors
+ if ( error ) {
+ throw new Error( `Telex generation failed: ${ error }` );
+ }
+
+ if ( ! artefactXml ) {
+ throw new Error( 'No artefact generated - response incomplete' );
+ }
+
+ return {
+ artefact: artefactXml,
+ epid,
+ chatText,
+ };
+ }
+
+ /**
+ * Fetch an existing block's artefact by project ID.
+ * Fetches BUILT files (compiled JS/CSS) ready for WordPress installation.
+ * Polls for build completion if block is still building.
+ *
+ * @param projectId - Can be either encoded project ID (epid) or public ID
+ * @param maxRetries - Maximum number of polling attempts (default: 60, ~2 minutes)
+ * @param onRetry - Optional callback called on each polling retry
+ * @returns Promise - Parsed artefact data with built files
+ */
+ async fetchBlock(
+ projectId: string,
+ maxRetries: number = TELEX_DEFAULTS.BUILD_MAX_RETRIES,
+ onRetry?: ( attempt: number, maxRetries: number ) => void
+ ): Promise< Artefact > {
+ const isEncodedId = projectId.startsWith( TELEX_DEFAULTS.ENCODED_ID_PREFIX );
+
+ // 1. Wait for build to complete
+ await this.waitForBuildCompletion( projectId, isEncodedId, maxRetries, onRetry );
+
+ // 2. Fetch built and source files
+ const builtFiles = await this.fetchBuiltFiles( projectId, isEncodedId );
+ const sourceFiles = await this.fetchSourceFiles( projectId, isEncodedId );
+
+ // 3. Combine all files
+ const allFiles = [ ...builtFiles, ...sourceFiles ];
+
+ // 4. Extract metadata and create artefact
+ return this.createArtefact( allFiles );
+ }
+
+ /**
+ * Wait for build to complete by polling the build endpoint
+ *
+ * @param projectId - Project ID
+ * @param isEncodedId - Whether ID is encoded or public
+ * @param maxRetries - Maximum polling attempts
+ * @param onRetry - Optional callback called on each retry with attempt number
+ */
+ private async waitForBuildCompletion(
+ projectId: string,
+ isEncodedId: boolean,
+ maxRetries: number,
+ onRetry?: ( attempt: number, maxRetries: number ) => void
+ ): Promise< void > {
+ let retries = 0;
+
+ while ( retries < maxRetries ) {
+ const buildUrl = this.getBuildUrl( projectId, isEncodedId );
+ const response = await fetch( buildUrl.toString(), {
+ method: 'GET',
+ headers: {
+ Authorization: `Bearer ${ this.wpcomToken }`,
+ },
+ } );
+
+ // If 204, build is not ready - wait and retry
+ if ( response.status === 204 ) {
+ retries++;
+ if ( retries >= maxRetries ) {
+ throw new Error( 'Build timeout: Block is taking too long to build.' );
+ }
+
+ // Notify about retry
+ onRetry?.( retries, maxRetries );
+
+ await new Promise( ( resolve ) => setTimeout( resolve, TELEX_DEFAULTS.BUILD_POLL_INTERVAL_MS ) );
+ continue;
+ }
+
+ // Check for errors
+ if ( ! response.ok ) {
+ const errorText = await response.text();
+ throw new Error(
+ `Telex API error (${ response.status }): ${ errorText.slice( 0, 200 ) }`
+ );
+ }
+
+ // Build is ready
+ break;
+ }
+ }
+
+ /**
+ * Fetch built files (compiled JS/CSS) from build endpoint
+ *
+ * @param projectId - Project ID
+ * @param isEncodedId - Whether ID is encoded or public
+ * @returns Array of built files
+ */
+ private async fetchBuiltFiles( projectId: string, isEncodedId: boolean ): Promise< ArtefactFile[] > {
+ const buildUrl = this.getBuildUrl( projectId, isEncodedId );
+ const response = await fetch( buildUrl.toString(), {
+ method: 'GET',
+ headers: {
+ Authorization: `Bearer ${ this.wpcomToken }`,
+ },
+ } );
+
+ if ( ! response.ok ) {
+ return [];
+ }
+
+ const contentType = response.headers.get( 'Content-Type' );
+ if ( ! contentType?.includes( 'application/xml' ) ) {
+ return [];
+ }
+
+ const xmlText = await response.text();
+ return this.parseBuiltFilesXml( xmlText );
+ }
+
+ /**
+ * Parse build XML to extract files
+ *
+ * @param xmlText - XML string with build files
+ * @returns Array of built files
+ */
+ private parseBuiltFilesXml( xmlText: string ): ArtefactFile[] {
+ const parser = new DOMParser();
+ const doc = parser.parseFromString( xmlText, 'text/xml' );
+
+ // Check for parse errors
+ const parseError = doc.getElementsByTagName( 'parsererror' );
+ if ( parseError.length > 0 ) {
+ throw new Error( `XML parsing error: ${ parseError[ 0 ].textContent }` );
+ }
+
+ const files: ArtefactFile[] = [];
+ const fileElements = doc.getElementsByTagName( 'file' );
+
+ for ( let i = 0; i < fileElements.length; i++ ) {
+ const fileEl = fileElements[ i ];
+ const name = fileEl.getAttribute( 'name' );
+ const content = fileEl.textContent || '';
+
+ if ( name ) {
+ files.push( {
+ path: `build/${ name }`,
+ content,
+ } );
+ }
+ }
+
+ return files;
+ }
+
+ /**
+ * Fetch source files (PHP, block.json, etc.) from artefact endpoint
+ *
+ * @param projectId - Project ID
+ * @param isEncodedId - Whether ID is encoded or public
+ * @returns Array of source files
+ */
+ private async fetchSourceFiles( projectId: string, isEncodedId: boolean ): Promise< ArtefactFile[] > {
+ const sourceUrl = this.getSourceUrl( projectId, isEncodedId );
+ const response = await fetch( sourceUrl.toString(), {
+ method: 'GET',
+ headers: {
+ Authorization: `Bearer ${ this.wpcomToken }`,
+ },
+ } );
+
+ if ( ! response.ok ) {
+ return [];
+ }
+
+ const sourceData = await response.json();
+ if ( sourceData.files && Array.isArray( sourceData.files ) ) {
+ return sourceData.files;
+ }
+
+ return [];
+ }
+
+ /**
+ * Create artefact object with metadata extracted from files
+ *
+ * @param files - All block files (built + source)
+ * @returns Complete artefact object
+ */
+ private createArtefact( files: ArtefactFile[] ): Artefact {
+ const blockJsonFile = files.find( ( f ) => f.path === 'src/block.json' );
+ const packageJsonFile = files.find( ( f ) => f.path === 'package.json' );
+
+ let name = 'Untitled Block';
+ let slug = 'untitled-block';
+
+ // Extract from package.json
+ if ( packageJsonFile ) {
+ try {
+ const pkg = JSON.parse( packageJsonFile.content );
+ name = pkg.name || name;
+ slug = pkg.name || slug;
+ } catch ( e ) {
+ console.warn( 'Warning: Could not parse package.json:', e instanceof Error ? e.message : 'Unknown error' );
+ }
+ }
+
+ // Extract from block.json (takes precedence)
+ if ( blockJsonFile ) {
+ try {
+ const blockMeta = JSON.parse( blockJsonFile.content );
+ if ( blockMeta.title ) {
+ name = blockMeta.title;
+ }
+ if ( blockMeta.name ) {
+ const parts = blockMeta.name.split( '/' );
+ const lastPart = parts[ parts.length - 1 ];
+ slug = lastPart.startsWith( 'block-' ) ? lastPart.substring( 6 ) : lastPart;
+ }
+ } catch ( e ) {
+ console.warn( 'Warning: Could not parse block.json:', e instanceof Error ? e.message : 'Unknown error' );
+ }
+ }
+
+ return {
+ name,
+ slug,
+ type: 'code-package',
+ schemaVersion: '2',
+ files,
+ };
+ }
+
+ /**
+ * Get build URL for a project
+ *
+ * @param projectId - Project ID
+ * @param isEncodedId - Whether ID is encoded or public
+ * @returns Build URL
+ */
+ private getBuildUrl( projectId: string, isEncodedId: boolean ): URL {
+ if ( isEncodedId ) {
+ const url = new URL( `${ this.apiUrl }/artefact` );
+ url.searchParams.set( 'epid', projectId );
+ return url;
+ } else {
+ return new URL( `${ this.apiUrl }/project/${ projectId }/artefact` );
+ }
+ }
+
+ /**
+ * Get source URL for a project
+ *
+ * @param projectId - Project ID
+ * @param isEncodedId - Whether ID is encoded or public
+ * @returns Source URL
+ */
+ private getSourceUrl( projectId: string, isEncodedId: boolean ): URL {
+ if ( isEncodedId ) {
+ const url = new URL( `${ this.apiUrl }/artefact` );
+ url.searchParams.set( 'epid', projectId );
+ url.searchParams.set( 'mode', 'artefact' );
+ return url;
+ } else {
+ const url = new URL( `${ this.apiUrl }/project/${ projectId }/artefact` );
+ url.searchParams.set( 'mode', 'artefact' );
+ return url;
+ }
+ }
+
+ /**
+ * Get the full URL for a project in the Telex web UI
+ */
+ getProjectUrl( epid: string ): string {
+ const webUrl = this.apiUrl.replace( /\/api$/, '' );
+ return `${ webUrl }/projects/${ epid }`;
+ }
+}
diff --git a/cli/lib/telex-command-utils.ts b/cli/lib/telex-command-utils.ts
new file mode 100644
index 000000000..f2760024e
--- /dev/null
+++ b/cli/lib/telex-command-utils.ts
@@ -0,0 +1,111 @@
+import { __, sprintf } from '@wordpress/i18n';
+import { getAuthToken, getSiteByFolder } from 'cli/lib/appdata';
+import ora from 'ora';
+import chalk from 'chalk';
+import { TelexClient, type Artefact } from 'cli/lib/telex-client';
+import { getBlockMetadata } from 'cli/lib/artefact-parser';
+import {
+ installBlockToSitePath,
+ getPluginActivationUrl,
+} from 'cli/lib/block-installer';
+import { getTelexApiUrl } from 'cli/lib/telex-constants';
+
+interface TelexCommandOptions {
+ // Reserved for future options
+}
+
+/**
+ * Shared command execution flow for Telex block commands
+ *
+ * Handles authentication, site detection, installation, and next steps.
+ * The only difference between commands is how they fetch the artefact.
+ *
+ * @param options - Command options
+ * @param sitePath - Path to WordPress site
+ * @param fetchArtefact - Callback to fetch the artefact (differs per command)
+ */
+export async function runTelexCommand(
+ options: TelexCommandOptions,
+ sitePath: string,
+ fetchArtefact: ( telex: TelexClient, spinner: ora.Ora ) => Promise< Artefact >
+): Promise< void > {
+ const spinner = ora();
+
+ try {
+ // 1. Check authentication
+ spinner.start( __( 'Checking authentication...' ) );
+ const authToken = await getAuthToken();
+ if ( ! authToken ) {
+ spinner.fail( __( 'Not authenticated with WordPress.com' ) );
+ console.log(
+ chalk.yellow( '\nPlease run:' ),
+ chalk.cyan( 'studio auth login' )
+ );
+ return;
+ }
+ spinner.succeed(
+ sprintf( __( 'Authenticated as %s' ), chalk.cyan( authToken.displayName ) )
+ );
+
+ // 2. Detect current site
+ spinner.start( __( 'Loading site...' ) );
+ const site = await getSiteByFolder( sitePath );
+ spinner.succeed( sprintf( __( 'Site: %s' ), chalk.cyan( site.name ) ) );
+
+ // 3. Initialize Telex client
+ const telexApiUrl = getTelexApiUrl();
+ const telex = new TelexClient( telexApiUrl, authToken.accessToken );
+
+ // 4. Fetch artefact (command-specific logic via callback)
+ const artefact = await fetchArtefact( telex, spinner );
+
+ // 5. Get block metadata
+ const metadata = getBlockMetadata( artefact );
+
+ // 6. Show block info
+ console.log( chalk.bold( '\nBlock Information:' ) );
+ console.log( chalk.gray( ' Name: ' ), chalk.white( artefact.name ) );
+ console.log( chalk.gray( ' Slug: ' ), chalk.white( artefact.slug ) );
+ console.log(
+ chalk.gray( ' Files: ' ),
+ chalk.white( artefact.files.length )
+ );
+
+ if ( metadata?.title ) {
+ console.log(
+ chalk.gray( ' Title: ' ),
+ chalk.white( metadata.title )
+ );
+ }
+
+ // 7. Install to current site
+ spinner.start( sprintf( __( 'Installing block to %s...' ), site.name ) );
+ await installBlockToSitePath( site.path, artefact );
+ spinner.succeed( sprintf( __( 'Block installed to %s' ), chalk.cyan( site.name ) ) );
+
+ // 8. Show next steps
+ const pluginUrl = getPluginActivationUrl( site.name, artefact.slug );
+
+ console.log( chalk.bold( '\n📦 Next Steps:' ) );
+ console.log( chalk.gray( ' 1.' ), 'Visit', chalk.cyan( pluginUrl ) );
+ console.log(
+ chalk.gray( ' 2.' ),
+ 'Activate',
+ chalk.cyan( `"${ artefact.name }"` )
+ );
+ console.log(
+ chalk.gray( ' 3.' ),
+ 'Create a post and add the block'
+ );
+ } catch ( error ) {
+ if ( spinner.isSpinning ) {
+ spinner.fail();
+ }
+
+ if ( error instanceof Error && error.message !== 'User force closed the prompt' ) {
+ console.error( chalk.red( '\nCommand failed:' ), error.message );
+ }
+
+ process.exit( 1 );
+ }
+}
diff --git a/cli/lib/telex-constants.ts b/cli/lib/telex-constants.ts
new file mode 100644
index 000000000..48f963cc6
--- /dev/null
+++ b/cli/lib/telex-constants.ts
@@ -0,0 +1,29 @@
+/**
+ * Telex API configuration constants
+ */
+export const TELEX_DEFAULTS = {
+ API_URL: 'https://telex.automattic.ai/api',
+ BUILD_POLL_INTERVAL_MS: 2000,
+ BUILD_MAX_RETRIES: 60, // ~2 minutes
+ ENCODED_ID_PREFIX: 'v1.',
+} as const;
+
+/**
+ * Get Telex API URL from environment variable or default
+ *
+ * Set STUDIO_TELEX_URL environment variable to use a custom Telex deployment
+ * (useful for development or testing against staging environments)
+ *
+ * @returns Resolved Telex API URL
+ */
+export function getTelexApiUrl(): string {
+ return process.env.STUDIO_TELEX_URL || TELEX_DEFAULTS.API_URL;
+}
+
+/**
+ * Studio site path conventions
+ */
+export const STUDIO_PATHS = {
+ SITES_DIR: 'Studio/sites',
+ DOMAIN_SUFFIX: '.local',
+} as const;
diff --git a/cli/package-lock.json b/cli/package-lock.json
index 2aa0fe160..3d672fba9 100644
--- a/cli/package-lock.json
+++ b/cli/package-lock.json
@@ -14,6 +14,7 @@
"@wp-playground/cli": "^3.0.22",
"@wp-playground/common": "^3.0.22",
"@wp-playground/storage": "^3.0.22",
+ "@xmldom/xmldom": "^0.9.5",
"http-proxy": "^1.18.1",
"pm2": "^6.0.13",
"trash": "^10.0.1"
@@ -1539,6 +1540,15 @@
}
}
},
+ "node_modules/@xmldom/xmldom": {
+ "version": "0.9.8",
+ "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.9.8.tgz",
+ "integrity": "sha512-p96FSY54r+WJ50FIOsCOjyj/wavs8921hG5+kVMmZgKcvIKxMXHTrjNJvRgWa/zuX3B6t2lijLNFaOyuxUH+2A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.6"
+ }
+ },
"node_modules/@zip.js/zip.js": {
"version": "2.7.57",
"resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.7.57.tgz",
diff --git a/cli/package.json b/cli/package.json
index e81185477..8f5daa01c 100644
--- a/cli/package.json
+++ b/cli/package.json
@@ -12,6 +12,7 @@
"@wp-playground/cli": "^3.0.22",
"@wp-playground/common": "^3.0.22",
"@wp-playground/storage": "^3.0.22",
+ "@xmldom/xmldom": "^0.9.5",
"http-proxy": "^1.18.1",
"pm2": "^6.0.13",
"trash": "^10.0.1"
diff --git a/package-lock.json b/package-lock.json
index 70146c823..898854fc5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -119,6 +119,7 @@
"jest-watch-typeahead": "^3.0.1",
"nock": "^13.5.6",
"patch-package": "^8.0.0",
+ "pm2": "^6.0.14",
"postcss": "^8.4.32",
"prettier": "npm:wp-prettier@3.0.3",
"react": "^18.2.0",
@@ -8301,6 +8302,334 @@
"node": ">=18"
}
},
+ "node_modules/@pm2/agent": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/@pm2/agent/-/agent-2.1.1.tgz",
+ "integrity": "sha512-0V9ckHWd/HSC8BgAbZSoq8KXUG81X97nSkAxmhKDhmF8vanyaoc1YXwc2KVkbWz82Rg4gjd2n9qiT3i7bdvGrQ==",
+ "dev": true,
+ "license": "AGPL-3.0",
+ "dependencies": {
+ "async": "~3.2.0",
+ "chalk": "~3.0.0",
+ "dayjs": "~1.8.24",
+ "debug": "~4.3.1",
+ "eventemitter2": "~5.0.1",
+ "fast-json-patch": "^3.1.0",
+ "fclone": "~1.0.11",
+ "pm2-axon": "~4.0.1",
+ "pm2-axon-rpc": "~0.7.0",
+ "proxy-agent": "~6.4.0",
+ "semver": "~7.5.0",
+ "ws": "~7.5.10"
+ }
+ },
+ "node_modules/@pm2/agent/node_modules/chalk": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
+ "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@pm2/agent/node_modules/dayjs": {
+ "version": "1.8.36",
+ "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.8.36.tgz",
+ "integrity": "sha512-3VmRXEtw7RZKAf+4Tv1Ym9AGeo8r8+CjDi26x+7SYQil1UqtqdaokhzoEJohqlzt0m5kacJSDhJQkG/LWhpRBw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@pm2/agent/node_modules/debug": {
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@pm2/agent/node_modules/lru-cache": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+ "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@pm2/agent/node_modules/semver": {
+ "version": "7.5.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
+ "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "lru-cache": "^6.0.0"
+ },
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@pm2/agent/node_modules/ws": {
+ "version": "7.5.10",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz",
+ "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.3.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": "^5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@pm2/agent/node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/@pm2/blessed": {
+ "version": "0.1.81",
+ "resolved": "https://registry.npmjs.org/@pm2/blessed/-/blessed-0.1.81.tgz",
+ "integrity": "sha512-ZcNHqQjMuNRcQ7Z1zJbFIQZO/BDKV3KbiTckWdfbUaYhj7uNmUwb+FbdDWSCkvxNr9dBJQwvV17o6QBkAvgO0g==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "blessed": "bin/tput.js"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/@pm2/io": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/@pm2/io/-/io-6.1.0.tgz",
+ "integrity": "sha512-IxHuYURa3+FQ6BKePlgChZkqABUKFYH6Bwbw7V/pWU1pP6iR1sCI26l7P9ThUEB385ruZn/tZS3CXDUF5IA1NQ==",
+ "dev": true,
+ "license": "Apache-2",
+ "dependencies": {
+ "async": "~2.6.1",
+ "debug": "~4.3.1",
+ "eventemitter2": "^6.3.1",
+ "require-in-the-middle": "^5.0.0",
+ "semver": "~7.5.4",
+ "shimmer": "^1.2.0",
+ "signal-exit": "^3.0.3",
+ "tslib": "1.9.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ }
+ },
+ "node_modules/@pm2/io/node_modules/async": {
+ "version": "2.6.4",
+ "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
+ "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "lodash": "^4.17.14"
+ }
+ },
+ "node_modules/@pm2/io/node_modules/debug": {
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@pm2/io/node_modules/eventemitter2": {
+ "version": "6.4.9",
+ "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.9.tgz",
+ "integrity": "sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@pm2/io/node_modules/lru-cache": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+ "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@pm2/io/node_modules/require-in-the-middle": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-5.2.0.tgz",
+ "integrity": "sha512-efCx3b+0Z69/LGJmm9Yvi4cqEdxnoGnxYxGxBghkkTTFeXRtTCmmhO0AnAfHz59k957uTSuy8WaHqOs8wbYUWg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.1.1",
+ "module-details-from-path": "^1.0.3",
+ "resolve": "^1.22.1"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@pm2/io/node_modules/semver": {
+ "version": "7.5.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
+ "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "lru-cache": "^6.0.0"
+ },
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@pm2/io/node_modules/tslib": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz",
+ "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/@pm2/io/node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/@pm2/js-api": {
+ "version": "0.8.0",
+ "resolved": "https://registry.npmjs.org/@pm2/js-api/-/js-api-0.8.0.tgz",
+ "integrity": "sha512-nmWzrA/BQZik3VBz+npRcNIu01kdBhWL0mxKmP1ciF/gTcujPTQqt027N9fc1pK9ERM8RipFhymw7RcmCyOEYA==",
+ "dev": true,
+ "license": "Apache-2",
+ "dependencies": {
+ "async": "^2.6.3",
+ "debug": "~4.3.1",
+ "eventemitter2": "^6.3.1",
+ "extrareqp2": "^1.0.0",
+ "ws": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/@pm2/js-api/node_modules/async": {
+ "version": "2.6.4",
+ "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
+ "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "lodash": "^4.17.14"
+ }
+ },
+ "node_modules/@pm2/js-api/node_modules/debug": {
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@pm2/js-api/node_modules/eventemitter2": {
+ "version": "6.4.9",
+ "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.9.tgz",
+ "integrity": "sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@pm2/js-api/node_modules/ws": {
+ "version": "7.5.10",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz",
+ "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.3.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": "^5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@pm2/pm2-version-check": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@pm2/pm2-version-check/-/pm2-version-check-1.0.4.tgz",
+ "integrity": "sha512-SXsM27SGH3yTWKc2fKR4SYNxsmnvuBQ9dd6QHtEWmiZ/VqaOYPAIlS8+vMcn27YLtAEBGvNRSh3TPNvtjZgfqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.3.1"
+ }
+ },
"node_modules/@prisma/instrumentation": {
"version": "6.11.1",
"resolved": "https://registry.npmjs.org/@prisma/instrumentation/-/instrumentation-6.11.1.tgz",
@@ -9855,6 +10184,13 @@
"node": ">= 10"
}
},
+ "node_modules/@tootallnate/quickjs-emscripten": {
+ "version": "0.23.0",
+ "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz",
+ "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@tsconfig/node10": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz",
@@ -12425,6 +12761,33 @@
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"dev": true
},
+ "node_modules/amp": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/amp/-/amp-0.3.1.tgz",
+ "integrity": "sha512-OwIuC4yZaRogHKiuU5WlMR5Xk/jAcpPtawWL05Gj8Lvm2F6mwoJt4O/bHI+DHwG79vWd+8OFYM4/BzYqyRd3qw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/amp-message": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/amp-message/-/amp-message-0.1.2.tgz",
+ "integrity": "sha512-JqutcFwoU1+jhv7ArgW38bqrE+LQdcRv4NxNw0mp0JHQyB6tXesWRjtYKlDgHRY2o3JE5UTaBGUK8kSWUdxWUg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "amp": "0.3.1"
+ }
+ },
+ "node_modules/ansi-colors": {
+ "version": "4.1.3",
+ "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
+ "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/ansi-escapes": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
@@ -12474,6 +12837,16 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
+ "node_modules/ansis": {
+ "version": "4.0.0-node10",
+ "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.0.0-node10.tgz",
+ "integrity": "sha512-BRrU0Bo1X9dFGw6KgGz6hWrqQuOlVEDOzkb0QSLZY9sXHqA7pNj7yHPVJRz7y/rj4EOJ3d/D5uxH+ee9leYgsg==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/any-promise": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
@@ -12789,6 +13162,19 @@
"node": ">= 6"
}
},
+ "node_modules/ast-types": {
+ "version": "0.13.4",
+ "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz",
+ "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/async": {
"version": "3.2.5",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz",
@@ -13071,6 +13457,16 @@
"baseline-browser-mapping": "dist/cli.js"
}
},
+ "node_modules/basic-ftp": {
+ "version": "5.0.5",
+ "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz",
+ "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
"node_modules/before-after-hook": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz",
@@ -13103,6 +13499,13 @@
"integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==",
"license": "MIT"
},
+ "node_modules/bodec": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/bodec/-/bodec-0.1.0.tgz",
+ "integrity": "sha512-Ylo+MAo5BDUq1KA3f3R/MFhh+g8cnHmo8bz3YPGhI1znrMaf77ol1sfvYJzsw3nTE+Y2GryfDxBaR+AqpAkEHQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/body-parser": {
"version": "1.20.3",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
@@ -13698,6 +14101,13 @@
"resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz",
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA=="
},
+ "node_modules/charm": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/charm/-/charm-0.1.2.tgz",
+ "integrity": "sha512-syedaZ9cPe7r3hoQA9twWYKu5AIyCswN5+szkmPBe9ccdLrj4bYaCnLVPTLd2kgVRc7+zoX4tyPgRnFKCj5YjQ==",
+ "dev": true,
+ "license": "MIT/X11"
+ },
"node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
@@ -13839,7 +14249,33 @@
"@colors/colors": "1.5.0"
}
},
- "node_modules/cli-truncate": {
+ "node_modules/cli-tableau": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/cli-tableau/-/cli-tableau-2.0.1.tgz",
+ "integrity": "sha512-he+WTicka9cl0Fg/y+YyxcN6/bfQ/1O3QmgxRXDhABKqLzvoOSM4fMzp39uMyLBulAFuywD2N7UaoQE7WaADxQ==",
+ "dev": true,
+ "dependencies": {
+ "chalk": "3.0.0"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/cli-tableau/node_modules/chalk": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
+ "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cli-truncate": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-3.1.0.tgz",
"integrity": "sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA==",
@@ -14321,6 +14757,13 @@
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ=="
},
+ "node_modules/croner": {
+ "version": "4.1.97",
+ "resolved": "https://registry.npmjs.org/croner/-/croner-4.1.97.tgz",
+ "integrity": "sha512-/f6gpQuxDaqXu+1kwQYSckUglPaOrHdbIlBAu0YuW8/Cdb45XwXYNUBXg3r/9Mo6n540Kn/smKcZWko5x99KrQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/cross-dirname": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/cross-dirname/-/cross-dirname-0.1.0.tgz",
@@ -14430,12 +14873,29 @@
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"license": "MIT"
},
+ "node_modules/culvert": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/culvert/-/culvert-0.1.2.tgz",
+ "integrity": "sha512-yi1x3EAWKjQTreYWeSd98431AV+IEE0qoDyOoaHJ7KJ21gv6HtBXHVLX74opVSGqcR8/AbjJBHAHpcOy2bj5Gg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/custom-error-instance": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/custom-error-instance/-/custom-error-instance-2.1.1.tgz",
"integrity": "sha512-p6JFxJc3M4OTD2li2qaHkDCw9SfMw82Ldr6OC9Je1aXiGfhx2W8p3GaoeaGrPJTUN9NirTM/KTxHWMUdR1rsUg==",
"license": "ISC"
},
+ "node_modules/data-uri-to-buffer": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz",
+ "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14"
+ }
+ },
"node_modules/data-urls": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz",
@@ -14519,6 +14979,13 @@
"integrity": "sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==",
"license": "MIT"
},
+ "node_modules/dayjs": {
+ "version": "1.11.15",
+ "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.15.tgz",
+ "integrity": "sha512-MC+DfnSWiM9APs7fpiurHGCoeIx0Gdl6QZBy+5lu8MbYKN5FZEXqOgrundfibdfhGZ15o9hzmZ2xJjZnbvgKXQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
@@ -14666,6 +15133,21 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/degenerator": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz",
+ "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ast-types": "^0.13.4",
+ "escodegen": "^2.1.0",
+ "esprima": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@@ -15552,6 +16034,19 @@
"node": ">=10.13.0"
}
},
+ "node_modules/enquirer": {
+ "version": "2.3.6",
+ "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz",
+ "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-colors": "^4.1.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
"node_modules/entities": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
@@ -15833,6 +16328,38 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/escodegen": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz",
+ "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esprima": "^4.0.1",
+ "estraverse": "^5.2.0",
+ "esutils": "^2.0.2"
+ },
+ "bin": {
+ "escodegen": "bin/escodegen.js",
+ "esgenerate": "bin/esgenerate.js"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "optionalDependencies": {
+ "source-map": "~0.6.1"
+ }
+ },
+ "node_modules/escodegen/node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
"node_modules/eslint": {
"version": "9.39.2",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz",
@@ -16373,6 +16900,13 @@
"node": ">= 0.6"
}
},
+ "node_modules/eventemitter2": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-5.0.1.tgz",
+ "integrity": "sha512-5EM1GHXycJBS6mauYAbVKT1cVs7POKWb2NXD4Vyt8dDqeZa7LaDK1/sjtL+Zb0lzTpSNil4596Dyu97hz37QLg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/eventemitter3": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
@@ -16711,6 +17245,16 @@
"fd-slicer": "~1.1.0"
}
},
+ "node_modules/extrareqp2": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/extrareqp2/-/extrareqp2-1.0.0.tgz",
+ "integrity": "sha512-Gum0g1QYb6wpPJCVypWP3bbIuaibcFiJcpuPM10YSXp/tzqi84x9PJageob+eN4xVRIOto4wjSGNLyMD54D2xA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "follow-redirects": "^1.14.0"
+ }
+ },
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -16756,6 +17300,13 @@
"node": ">= 6"
}
},
+ "node_modules/fast-json-patch": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/fast-json-patch/-/fast-json-patch-3.1.1.tgz",
+ "integrity": "sha512-vf6IHUX2SBcA+5/+4883dsIjpBTqmfBjmYiWK1savxQmFk4JfBMLa7ynTYOs1Rolp/T1betJxHiGD3g1Mn8lUQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/fast-json-stable-stringify": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
@@ -16827,6 +17378,13 @@
"bser": "2.1.1"
}
},
+ "node_modules/fclone": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/fclone/-/fclone-1.0.11.tgz",
+ "integrity": "sha512-GDqVQezKzRABdeqflsgMr7ktzgF9CyS+p2oe0jJqUY6izSSbhPIQJDpoU4PtGcD7VPM9xh/dVrTu6z1nwgmEGw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/fd-slicer": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
@@ -17490,6 +18048,21 @@
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
}
},
+ "node_modules/get-uri": {
+ "version": "6.0.5",
+ "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz",
+ "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "basic-ftp": "^5.0.2",
+ "data-uri-to-buffer": "^6.0.2",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
"node_modules/gettext-parser": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/gettext-parser/-/gettext-parser-1.4.0.tgz",
@@ -17499,6 +18072,20 @@
"safe-buffer": "^5.1.1"
}
},
+ "node_modules/git-node-fs": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/git-node-fs/-/git-node-fs-1.0.0.tgz",
+ "integrity": "sha512-bLQypt14llVXBg0S0u8q8HmU7g9p3ysH+NvVlae5vILuUvs759665HvmR5+wb04KjHyjFcDRxdYb4kyNnluMUQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/git-sha1": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/git-sha1/-/git-sha1-0.1.2.tgz",
+ "integrity": "sha512-2e/nZezdVlyCopOCYHeW0onkbZg7xP1Ad6pndPy1rCygeRykefUS6r7oA5cJRGEFvseiaz5a/qUHFVX1dd6Isg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
@@ -20698,16 +21285,37 @@
"integrity": "sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==",
"license": "BSD-3-Clause"
},
+ "node_modules/js-git": {
+ "version": "0.7.8",
+ "resolved": "https://registry.npmjs.org/js-git/-/js-git-0.7.8.tgz",
+ "integrity": "sha512-+E5ZH/HeRnoc/LW0AmAyhU+mNcWBzAKE+30+IDMLSLbbK+Tdt02AdkOKq9u15rlJsDEGFqtgckc8ZM59LhhiUA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "bodec": "^0.1.0",
+ "culvert": "^0.1.2",
+ "git-sha1": "^0.1.2",
+ "pako": "^0.2.5"
+ }
+ },
+ "node_modules/js-git/node_modules/pako": {
+ "version": "0.2.9",
+ "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz",
+ "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
},
"node_modules/js-yaml": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
- "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
+ "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"argparse": "^2.0.1"
},
@@ -22972,6 +23580,34 @@
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
"dev": true
},
+ "node_modules/needle": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/needle/-/needle-2.4.0.tgz",
+ "integrity": "sha512-4Hnwzr3mi5L97hMYeNl8wRW/Onhy4nUKR/lVemJ8gJedxxUyBLm9kkrDColJvoSfwi0jCNhD+xCdOtiGDQiRZg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^3.2.6",
+ "iconv-lite": "^0.4.4",
+ "sax": "^1.2.4"
+ },
+ "bin": {
+ "needle": "bin/needle"
+ },
+ "engines": {
+ "node": ">= 4.4.x"
+ }
+ },
+ "node_modules/needle/node_modules/debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
"node_modules/negotiator": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
@@ -22986,6 +23622,16 @@
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
"dev": true
},
+ "node_modules/netmask": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz",
+ "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
"node_modules/nice-try": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
@@ -23601,6 +24247,93 @@
"node": ">=6"
}
},
+ "node_modules/pac-proxy-agent": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz",
+ "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@tootallnate/quickjs-emscripten": "^0.23.0",
+ "agent-base": "^7.1.2",
+ "debug": "^4.3.4",
+ "get-uri": "^6.0.1",
+ "http-proxy-agent": "^7.0.0",
+ "https-proxy-agent": "^7.0.6",
+ "pac-resolver": "^7.0.1",
+ "socks-proxy-agent": "^8.0.5"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/pac-proxy-agent/node_modules/agent-base": {
+ "version": "7.1.4",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
+ "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/pac-proxy-agent/node_modules/http-proxy-agent": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
+ "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/pac-proxy-agent/node_modules/https-proxy-agent": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
+ "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.2",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/pac-proxy-agent/node_modules/socks-proxy-agent": {
+ "version": "8.0.5",
+ "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz",
+ "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.2",
+ "debug": "^4.3.4",
+ "socks": "^2.8.3"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/pac-resolver": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz",
+ "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "degenerator": "^5.0.0",
+ "netmask": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
"node_modules/package-json-from-dist": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz",
@@ -23960,6 +24693,40 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
+ "node_modules/pidusage": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/pidusage/-/pidusage-3.0.2.tgz",
+ "integrity": "sha512-g0VU+y08pKw5M8EZ2rIGiEBaB8wrQMjYGFfW2QVIfyT8V+fq8YFLkvlz4bz5ljvFDJYNFCWT3PWqcRr2FKO81w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "^5.2.1"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/pidusage/node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
"node_modules/pify": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
@@ -24104,6 +24871,194 @@
"node": ">=10.4.0"
}
},
+ "node_modules/pm2": {
+ "version": "6.0.14",
+ "resolved": "https://registry.npmjs.org/pm2/-/pm2-6.0.14.tgz",
+ "integrity": "sha512-wX1FiFkzuT2H/UUEA8QNXDAA9MMHDsK/3UHj6Dkd5U7kxyigKDA5gyDw78ycTQZAuGCLWyUX5FiXEuVQWafukA==",
+ "dev": true,
+ "license": "AGPL-3.0",
+ "dependencies": {
+ "@pm2/agent": "~2.1.1",
+ "@pm2/blessed": "0.1.81",
+ "@pm2/io": "~6.1.0",
+ "@pm2/js-api": "~0.8.0",
+ "@pm2/pm2-version-check": "^1.0.4",
+ "ansis": "4.0.0-node10",
+ "async": "3.2.6",
+ "chokidar": "3.6.0",
+ "cli-tableau": "2.0.1",
+ "commander": "2.15.1",
+ "croner": "4.1.97",
+ "dayjs": "1.11.15",
+ "debug": "4.4.3",
+ "enquirer": "2.3.6",
+ "eventemitter2": "5.0.1",
+ "fclone": "1.0.11",
+ "js-yaml": "4.1.1",
+ "mkdirp": "1.0.4",
+ "needle": "2.4.0",
+ "pidusage": "3.0.2",
+ "pm2-axon": "~4.0.1",
+ "pm2-axon-rpc": "~0.7.1",
+ "pm2-deploy": "~1.0.2",
+ "pm2-multimeter": "^0.1.2",
+ "promptly": "2.2.0",
+ "semver": "7.7.2",
+ "source-map-support": "0.5.21",
+ "sprintf-js": "1.1.2",
+ "vizion": "~2.2.1"
+ },
+ "bin": {
+ "pm2": "bin/pm2",
+ "pm2-dev": "bin/pm2-dev",
+ "pm2-docker": "bin/pm2-docker",
+ "pm2-runtime": "bin/pm2-runtime"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ },
+ "optionalDependencies": {
+ "pm2-sysmonit": "^1.2.8"
+ }
+ },
+ "node_modules/pm2-axon": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/pm2-axon/-/pm2-axon-4.0.1.tgz",
+ "integrity": "sha512-kES/PeSLS8orT8dR5jMlNl+Yu4Ty3nbvZRmaAtROuVm9nYYGiaoXqqKQqQYzWQzMYWUKHMQTvBlirjE5GIIxqg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "amp": "~0.3.1",
+ "amp-message": "~0.1.1",
+ "debug": "^4.3.1",
+ "escape-string-regexp": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=5"
+ }
+ },
+ "node_modules/pm2-axon-rpc": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/pm2-axon-rpc/-/pm2-axon-rpc-0.7.1.tgz",
+ "integrity": "sha512-FbLvW60w+vEyvMjP/xom2UPhUN/2bVpdtLfKJeYM3gwzYhoTEEChCOICfFzxkxuoEleOlnpjie+n1nue91bDQw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=5"
+ }
+ },
+ "node_modules/pm2-deploy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/pm2-deploy/-/pm2-deploy-1.0.2.tgz",
+ "integrity": "sha512-YJx6RXKrVrWaphEYf++EdOOx9EH18vM8RSZN/P1Y+NokTKqYAca/ejXwVLyiEpNju4HPZEk3Y2uZouwMqUlcgg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "run-series": "^1.1.8",
+ "tv4": "^1.3.0"
+ },
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/pm2-multimeter": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/pm2-multimeter/-/pm2-multimeter-0.1.2.tgz",
+ "integrity": "sha512-S+wT6XfyKfd7SJIBqRgOctGxaBzUOmVQzTAS+cg04TsEUObJVreha7lvCfX8zzGVr871XwCSnHUU7DQQ5xEsfA==",
+ "dev": true,
+ "license": "MIT/X11",
+ "dependencies": {
+ "charm": "~0.1.1"
+ }
+ },
+ "node_modules/pm2-sysmonit": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/pm2-sysmonit/-/pm2-sysmonit-1.2.8.tgz",
+ "integrity": "sha512-ACOhlONEXdCTVwKieBIQLSi2tQZ8eKinhcr9JpZSUAL8Qy0ajIgRtsLxG/lwPOW3JEKqPyw/UaHmTWhUzpP4kA==",
+ "dev": true,
+ "license": "Apache",
+ "optional": true,
+ "dependencies": {
+ "async": "^3.2.0",
+ "debug": "^4.3.1",
+ "pidusage": "^2.0.21",
+ "systeminformation": "^5.7",
+ "tx2": "~1.0.4"
+ }
+ },
+ "node_modules/pm2-sysmonit/node_modules/pidusage": {
+ "version": "2.0.21",
+ "resolved": "https://registry.npmjs.org/pidusage/-/pidusage-2.0.21.tgz",
+ "integrity": "sha512-cv3xAQos+pugVX+BfXpHsbyz/dLzX+lr44zNMsYiGxUw+kV5sgQCIcLd1z+0vq+KyC7dJ+/ts2PsfgWfSC3WXA==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "safe-buffer": "^5.2.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/pm2-sysmonit/node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/pm2/node_modules/async": {
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
+ "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/pm2/node_modules/commander": {
+ "version": "2.15.1",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz",
+ "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/pm2/node_modules/semver": {
+ "version": "7.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
+ "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/pm2/node_modules/sprintf-js": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz",
+ "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
"node_modules/possible-typed-array-names": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz",
@@ -24440,6 +25395,16 @@
"node": ">=10"
}
},
+ "node_modules/promptly": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/promptly/-/promptly-2.2.0.tgz",
+ "integrity": "sha512-aC9j+BZsRSSzEsXBNBwDnAxujdx19HycZoKgRgzWnS8eOHg1asuf9heuLprfbe739zY3IdUQx+Egv6Jn135WHA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "read": "^1.0.4"
+ }
+ },
"node_modules/propagate": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz",
@@ -24492,6 +25457,89 @@
"node": ">= 0.10"
}
},
+ "node_modules/proxy-agent": {
+ "version": "6.4.0",
+ "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.4.0.tgz",
+ "integrity": "sha512-u0piLU+nCOHMgGjRbimiXmA9kM/L9EHh3zL81xCdp7m+Y2pHIsnmbdDoEDoAz5geaonNR6q6+yOPQs6n4T6sBQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.0.2",
+ "debug": "^4.3.4",
+ "http-proxy-agent": "^7.0.1",
+ "https-proxy-agent": "^7.0.3",
+ "lru-cache": "^7.14.1",
+ "pac-proxy-agent": "^7.0.1",
+ "proxy-from-env": "^1.1.0",
+ "socks-proxy-agent": "^8.0.2"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/proxy-agent/node_modules/agent-base": {
+ "version": "7.1.4",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
+ "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/proxy-agent/node_modules/http-proxy-agent": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
+ "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/proxy-agent/node_modules/https-proxy-agent": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
+ "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.2",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/proxy-agent/node_modules/lru-cache": {
+ "version": "7.18.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
+ "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/proxy-agent/node_modules/socks-proxy-agent": {
+ "version": "8.0.5",
+ "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz",
+ "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.2",
+ "debug": "^4.3.4",
+ "socks": "^2.8.3"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
@@ -24776,6 +25824,19 @@
"node": ">=0.10.0"
}
},
+ "node_modules/read": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz",
+ "integrity": "sha512-rSOKNYUmaxy0om1BNjMN4ezNT6VKK+2xF4GBhc81mkH7L60i6dp8qPYrkndNLT3QPphoII3maL9PVC9XmhHwVQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "mute-stream": "~0.0.4"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
"node_modules/read-binary-file-arch": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz",
@@ -25519,6 +26580,27 @@
"queue-microtask": "^1.2.2"
}
},
+ "node_modules/run-series": {
+ "version": "1.1.9",
+ "resolved": "https://registry.npmjs.org/run-series/-/run-series-1.1.9.tgz",
+ "integrity": "sha512-Arc4hUN896vjkqCYrUXquBFtRZdv1PfLbTYP71efP6butxyQ0kWpiNJyAgsxscmQg1cqvHY32/UCBzXedTpU2g==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
"node_modules/rungen": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/rungen/-/rungen-0.3.2.tgz",
@@ -26854,6 +27936,34 @@
"url": "https://opencollective.com/synckit"
}
},
+ "node_modules/systeminformation": {
+ "version": "5.27.16",
+ "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.27.16.tgz",
+ "integrity": "sha512-aimHO/bE7QFtu3uB3vtpwn7V2DXXGX7NyTY7V1g+hPa7in2k10Bp3AL+Enmg3X71n7HbgLfwy/bbf+2cBSKURQ==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin",
+ "linux",
+ "win32",
+ "freebsd",
+ "openbsd",
+ "netbsd",
+ "sunos",
+ "android"
+ ],
+ "bin": {
+ "systeminformation": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ },
+ "funding": {
+ "type": "Buy me a coffee",
+ "url": "https://www.buymeacoffee.com/systeminfo"
+ }
+ },
"node_modules/tailwindcss": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.1.tgz",
@@ -27656,6 +28766,36 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/tv4": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/tv4/-/tv4-1.3.0.tgz",
+ "integrity": "sha512-afizzfpJgvPr+eDkREK4MxJ/+r8nEEHcmitwgnPUqpaP+FpwQyadnxNoSACbgc/b1LsZYtODGoPiFxQrgJgjvw==",
+ "dev": true,
+ "license": [
+ {
+ "type": "Public Domain",
+ "url": "http://geraintluff.github.io/tv4/LICENSE.txt"
+ },
+ {
+ "type": "MIT",
+ "url": "http://jsonary.com/LICENSE.txt"
+ }
+ ],
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/tx2": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/tx2/-/tx2-1.0.5.tgz",
+ "integrity": "sha512-sJ24w0y03Md/bxzK4FU8J8JveYYUbSs2FViLJ2D/8bytSiyPRbuE3DyL/9UKYXTZlV3yXq0L8GLlhobTnekCVg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "json-stringify-safe": "^5.0.1"
+ }
+ },
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@@ -29009,6 +30149,39 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
+ "node_modules/vizion": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/vizion/-/vizion-2.2.1.tgz",
+ "integrity": "sha512-sfAcO2yeSU0CSPFI/DmZp3FsFE9T+8913nv1xWBOyzODv13fwkn6Vl7HqxGpkr9F608M+8SuFId3s+BlZqfXww==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "async": "^2.6.3",
+ "git-node-fs": "^1.0.0",
+ "ini": "^1.3.5",
+ "js-git": "^0.7.8"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/vizion/node_modules/async": {
+ "version": "2.6.4",
+ "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
+ "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "lodash": "^4.17.14"
+ }
+ },
+ "node_modules/vizion/node_modules/ini": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
+ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
+ "dev": true,
+ "license": "ISC"
+ },
"node_modules/w3c-xmlserializer": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
diff --git a/package.json b/package.json
index 3a9a28cbd..502fe6c2d 100644
--- a/package.json
+++ b/package.json
@@ -89,6 +89,7 @@
"jest-watch-typeahead": "^3.0.1",
"nock": "^13.5.6",
"patch-package": "^8.0.0",
+ "pm2": "^6.0.14",
"postcss": "^8.4.32",
"prettier": "npm:wp-prettier@3.0.3",
"react": "^18.2.0",