Skip to content

CLI: Add jetpack playground command for quick WordPress Playground sessions#47597

Open
enejb wants to merge 2 commits intotrunkfrom
try/playground
Open

CLI: Add jetpack playground command for quick WordPress Playground sessions#47597
enejb wants to merge 2 commits intotrunkfrom
try/playground

Conversation

@enejb
Copy link
Member

@enejb enejb commented Mar 14, 2026

This PR make it possible to quickly spin up a playground instance. This could be useful for LLM agents to check their work.

The initial step might

See

Screen.Recording.2026-03-16.at.2.54.34.PM.mov

Proposed changes

  • Add a new jetpack playground <plugin> CLI command that starts a local WordPress Playground instance with the specified plugin mounted
  • Uses jetpack rsync to create a temporary copy with monorepo symlinks resolved (vendor/jetpack_vendor symlinks point outside the plugin directory and break in Playground's sandbox)
  • Automatically injects JETPACK_DEV_DEBUG via a blueprint step so Jetpack runs in offline mode without requiring a WordPress.com connection
  • Auto-discovers plugin blueprints at .wordpress-org/blueprints/blueprint.json (e.g., CRM already has one)
  • Runs jetpack install + jetpack build automatically if the plugin hasn't been built yet
  • Cleans up temporary files when Playground exits

Other information

  • Generate changelog entries for this PR (using AI).

Related product discussion/links

Does this pull request change what data or activity we track or use?

No.

Testing instructions

  • Ensure you have Node.js installed and the monorepo set up (jetpack cli link)
  • Run jetpack playground jetpack
    • Verify it syncs plugin files, starts Playground, and Jetpack loads in offline mode (no connection prompt)
  • Run jetpack playground crm
    • Verify it picks up the existing CRM blueprint at projects/plugins/crm/.wordpress-org/blueprints/blueprint.json
  • Run jetpack playground nonexistent
    • Verify it shows an error with the list of available plugins
  • Run jetpack playground jetpack -p 9999
    • Verify Playground starts on port 9999
  • After stopping Playground (Ctrl+C), verify the temp directory is cleaned up

…sessions

Adds a new `jetpack playground <plugin>` command that spins up a local
WordPress Playground instance with the specified plugin mounted. This
provides a fast, Docker-free alternative for quick plugin testing.

The command:
- Uses `jetpack rsync` to create a resolved copy (fixing monorepo symlinks)
- Auto-discovers blueprints at `.wordpress-org/blueprints/blueprint.json`
- Injects JETPACK_DEV_DEBUG for offline mode (no WP.com connection needed)
- Auto-runs install/build if the plugin hasn't been built yet
- Cleans up temp files on exit

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions github-actions bot added the [Tools] Development CLI The tools/cli to assist during JP development. label Mar 14, 2026
@github-actions
Copy link
Contributor

github-actions bot commented Mar 14, 2026

Thank you for your PR!

When contributing to Jetpack, we have a few suggestions that can help us test and review your patch:

  • ✅ Include a description of your PR changes.
  • ✅ Add a "[Status]" label (In Progress, Needs Review, ...).
  • ✅ Add testing instructions.
  • ✅ Specify whether this PR includes any changes to data or privacy.
  • ✅ Add changelog entries to affected projects

This comment will be updated as you work on your PR and make changes. If you think that some of those checks are not needed for your PR, please explain why you think so. Thanks for cooperation 🤖


Follow this PR Review Process:

  1. Ensure all required checks appearing at the bottom of this PR are passing.
  2. Make sure to test your changes on all platforms that it applies to. You're responsible for the quality of the code you ship.
  3. You can use GitHub's Reviewers functionality to request a review.
  4. When it's reviewed and merged, you will be pinged in Slack to deploy the changes to WordPress.com simple once the build is done.

If you have questions about anything, reach out in #jetpack-developers for guidance!

@github-actions github-actions bot added the [Status] Needs Author Reply We need more details from you. This label will be auto-added until the PR meets all requirements. label Mar 14, 2026
@enejb enejb changed the title CLI: Add jetpack playground command CLI: Add jetpack playground command for quick WordPress Playground sessions Mar 14, 2026
@enejb enejb added the [Status] Needs Review This PR is ready for review. label Mar 14, 2026
@enejb enejb requested review from anomiex and Copilot March 16, 2026 15:01
@enejb
Copy link
Member Author

enejb commented Mar 16, 2026

@claude review this PR

@claude
Copy link

claude bot commented Mar 16, 2026

Claude finished @enejb's task in 2m 1s —— View job


PR Review: CLI: Add jetpack playground command

  • Gather context and read changed files
  • Analyze code for bugs, security issues, and best practices
  • Post review feedback

Overall this is a clean, well-structured addition. The concept is solid — using jetpack rsync to resolve monorepo symlinks before mounting in Playground is the right approach. A few issues worth addressing below.


Security: Command injection risk (playground.js:176–180)

The plugin name from argv is passed directly as an argument to spawnSync with { shell: true }. With shell: true, Node.js joins the array into a shell command string (e.g., /bin/sh -c "jetpack rsync PLUGINNAME ..."), making user-controlled input vulnerable to shell injection.

const rsyncResult = child_process.spawnSync(
    'jetpack',
    [ 'rsync', options.plugin, mountPath, '--non-interactive' ],
    { shell: true, stdio: 'inherit' }  // ← unsafe with user input
);

The same pattern applies to the install and build calls at lines 146–163. While the fs.existsSync check at line 127 would catch most malicious inputs (e.g., foo; rm -rf / won't resolve to an existing plugin), this is defense-by-accident rather than intentional validation. Either:

  • Drop shell: true (safe since you're passing an array), or
  • Validate plugin name against allProjectsByType('plugins') like rsync.js does (line 753 of rsync.js)

Fix this →


Bug: Vendor directory check is too strict (playground.js:137–139)

if ( ! fs.existsSync( vendorPath ) || ! fs.existsSync( jetpackVendorPath ) ) {

This requires both vendor/ and jetpack_vendor/ to exist. Many plugins legitimately have only one or neither (e.g., pure JS plugins without PHP Composer deps). This will trigger unnecessary install/build runs for those plugins.

Consider checking the plugin's composer.json to determine what's expected, or checking if either vendor/ or jetpack_vendor/ is missing only if it's expected to exist (e.g., if composer.json is present).

Fix this →


Bug: Blueprint steps merge is inconsistent with the comment (playground.js:95–101)

The code comment says "custom steps run first, then ours" but the base blueprint's steps: [] is discarded:

blueprint = {
    ...blueprint,
    ...custom,
    // Merge steps — custom steps run first, then ours.
    steps: [ ...( custom.steps || [] ) ],   // ← base steps not included
};

Since the base steps starts empty this doesn't cause a bug today, but if a future default step is added to the base blueprint it would be silently dropped. More importantly, this doesn't merge custom top-level steps that may have been defined in the base — the spread ...custom runs first, then the explicit steps key overrides. The intent seems correct, but the comment should be clarified or the code restructured to be more explicit.


Style: spawnSync in an async context (playground.js:146, 156, 207)

All three child_process.spawnSync calls block the event loop. The rest of the CLI codebase uses execa for subprocess invocation (see rsync.js). This isn't a functional bug but is inconsistent and makes the code harder to extend.


Minor: verbose check uses wrong property name (playground.js:201)

if ( options.verbose ) {

The global flag defined in cliRouter.js is -v / --verbose, which yargs exposes as both options.v and options.verbose. Other commands (e.g., rsync.js:149) use argv.v. This should still work, but for consistency with the codebase, consider using options.v || options.verbose.


Minor: No --verbose option in yargs builder (playground.js:19–36)

The verbose output path at line 201 is reachable but not discoverable since --verbose isn't listed in this command's help. The global -v flag in cliRouter.js handles this, but it would be cleaner to explicitly note it in the examples.


Suggestion: @wp-playground/cli is unpinned (playground.js:195)

const args = [ '@wp-playground/cli', 'server', '--auto-mount', ... ];

Running npx @wp-playground/cli without a version always fetches the latest. A breaking Playground CLI update would silently break this command for all users. Consider pinning to a version range or documenting the minimum required version.


Summary: The core logic is sound. The two most important things to fix before merging are the command injection risk (use shell: false or validate plugin name with allProjectsByType) and the overly strict vendor directory check. The rest are style/minor concerns.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a new jetpack playground <plugin> CLI subcommand to spin up a WordPress Playground instance with a monorepo plugin mounted, including optional custom blueprint and port selection.

Changes:

  • Introduces tools/cli/commands/playground.js implementing the Playground command flow (build check, rsync to temp dir, generate/merge blueprint, run Playground server).
  • Registers the new playground subcommand in the CLI router.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.

File Description
tools/cli/commands/playground.js New CLI subcommand to run WP Playground with a mounted plugin and merged blueprint generation.
tools/cli/cliRouter.js Wires the new playground command into the main jetpack CLI.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +146 to +150
const install = child_process.spawnSync(
'jetpack',
[ 'install', `plugins/${ options.plugin }` ],
{ shell: true, stdio: 'inherit' }
);
Comment on lines +124 to +135
const pluginPath = path.resolve( `projects/plugins/${ options.plugin }` );

// Validate that the plugin exists.
if ( ! fs.existsSync( pluginPath ) ) {
const available = dirs( './projects/plugins' );
console.error( chalk.red( `Plugin "${ options.plugin }" not found.` ) );
if ( available.length > 0 ) {
console.error( `\nAvailable plugins: ${ available.join( ', ' ) }` );
}
process.exit( 1 );
}

Comment on lines +174 to +206
console.log( chalk.gray( 'Syncing plugin files (resolving monorepo symlinks)...' ) );

const rsyncResult = child_process.spawnSync(
'jetpack',
[ 'rsync', options.plugin, mountPath, '--non-interactive' ],
{ shell: true, stdio: 'inherit' }
);
if ( rsyncResult.status !== 0 ) {
console.error( chalk.red( 'Failed to sync plugin files.' ) );
fs.rmSync( tmpDir, { recursive: true, force: true } );
process.exit( 1 );
}

// Build the blueprint. We always inject a step to enable Jetpack offline mode
// (JETPACK_DEV_DEBUG) so the plugin works without a WordPress.com connection.
const blueprintPath = buildBlueprint( pluginPath, mountPath, options );

console.log(
chalkJetpackGreen( `Starting WordPress Playground with plugins/${ options.plugin }...` )
);

const args = [ '@wp-playground/cli', 'server', '--auto-mount', `--blueprint=${ blueprintPath }` ];

if ( options.port ) {
args.push( `--port=${ options.port }` );
}

if ( options.verbose ) {
console.log( chalk.gray( `Running: npx ${ args.join( ' ' ) }` ) );
console.log( chalk.gray( `Synced copy: ${ mountPath }` ) );
}

try {
Comment on lines +92 to +100
if ( sourceBlueprint ) {
console.log( chalk.gray( `Using blueprint: ${ path.relative( '.', sourceBlueprint ) }` ) );
const custom = JSON.parse( fs.readFileSync( sourceBlueprint, 'utf8' ) );
blueprint = {
...blueprint,
...custom,
// Merge steps — custom steps run first, then ours.
steps: [ ...( custom.steps || [] ) ],
};

const args = [ '@wp-playground/cli', 'server', '--auto-mount', `--blueprint=${ blueprintPath }` ];

if ( options.port ) {
Copy link
Contributor

@anomiex anomiex left a comment

Choose a reason for hiding this comment

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

Uses jetpack rsync to create a temporary copy with monorepo symlinks resolved (vendor/jetpack_vendor symlinks point outside the plugin directory and break in Playground's sandbox)

Does Playground's --follow-symlinks option not work for you? It seems to work for me locally if I do like

npx @wp-playground/cli server --mount=projects/plugins/beta:/wordpress/wp-content/plugins/jetpack-beta --follow-symlinks

And that also means I can edit code in projects/plugins/beta and have Playground pick up the change immediately, while the rsync as you've coded it won't do that.

And if that doesn't work, it also seems to work to do like

npx @wp-playground/cli server --mount=.:/srv/jetpack-monorepo --blueprint=/path/to/blueprint.json

where the blueprint includes a step

{
	"step": "runPHP",
	"code": "<?php symlink( '/srv/jetpack-monorepo/projects/plugins/beta', '/wordpress/wp-content/plugins/jetpack-beta' ); ?>"
}

* @param {object} yargs - The Yargs dependency.
* @return {object} Yargs with the playground commands defined.
*/
export function playgroundDefine( yargs ) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I note this is the older style of making a subcommand module. It works, but you might consider changing it to the newer module style.

landingPage: '/wp-admin/',
login: true,
preferredVersions: {
php: '8.0',
Copy link
Contributor

Choose a reason for hiding this comment

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

Why 8.0?

Copy link
Member Author

Choose a reason for hiding this comment

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

No good reason. I think 8.3 works better.

- Convert to newer module style (command/describe/builder/handler exports)
- Use `projectDir()` and `maybePromptForPlugin()` from existing helpers
- Remove `shell: true` from all subprocess calls
- Remove hardcoded PHP 8.0, use Playground default
- Fix blueprint step merging to actually merge both arrays
- Read wp-plugin-slug from composer.json for correct mount paths
- Mount plugin + packages dirs so vendor symlinks resolve correctly
- Add mu-plugin to fix plugins_url() for symlink-resolved paths
- Auto-detect and activate the plugin's main PHP file
- Auto-build if plugin hasn't been built yet
- Validate port range and blueprint file existence
- Wrap temp dir in try/finally for cleanup on failure
- Show admin URL, login credentials, and quit instructions after startup
- Warn on first run if @wp-playground/cli needs downloading

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@enejb
Copy link
Member Author

enejb commented Mar 16, 2026

Thanks for all the feedback @anomiex!

Can do you mind taking another look?

I think next would be to add more helpful blueprint files. ( for example right now when you enable jetpack the form module is not enabled but that could be fixed with the blueprint file.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Status] Needs Author Reply We need more details from you. This label will be auto-added until the PR meets all requirements. [Status] Needs Review This PR is ready for review. [Tools] Development CLI The tools/cli to assist during JP development.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants