From f7df1b958d8c8ee856f0a6db325ce145e7988c37 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Oct 2025 12:12:45 +0000 Subject: [PATCH 1/7] Initial plan From b921fd8a78665a5db7a7668f0b160faf54a581da Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Oct 2025 12:22:48 +0000 Subject: [PATCH 2/7] Add restart command to shell REPL Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- features/shell.feature | 19 +++++++++++++++++++ src/Shell_Command.php | 12 ++++++++++-- src/WP_CLI/Shell/REPL.php | 8 ++++++++ 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/features/shell.feature b/features/shell.feature index 301c61bc..5d1d9d54 100644 --- a/features/shell.feature +++ b/features/shell.feature @@ -60,3 +60,22 @@ Feature: WordPress REPL bool(true) """ And STDERR should be empty + + Scenario: Restart shell + Given a WP install + And a session file: + """ + $a = 1; + restart + $b = 2; + """ + + When I run `wp shell --basic < session` + Then STDOUT should contain: + """ + Restarting shell... + """ + And STDOUT should contain: + """ + => int(2) + """ diff --git a/src/Shell_Command.php b/src/Shell_Command.php index edd2c611..00ec1103 100644 --- a/src/Shell_Command.php +++ b/src/Shell_Command.php @@ -25,6 +25,12 @@ class Shell_Command extends WP_CLI_Command { * $ wp shell * wp> get_bloginfo( 'name' ); * => string(6) "WP-CLI" + * + * # Restart the shell to reload code changes. + * $ wp shell + * wp> restart + * Restarting shell... + * wp> */ public function __invoke( $_, $assoc_args ) { $class = WP_CLI\Shell\REPL::class; @@ -55,8 +61,10 @@ public function __invoke( $_, $assoc_args ) { /** * @var class-string $class */ - $repl = new $class( 'wp> ' ); - $repl->start(); + do { + $repl = new $class( 'wp> ' ); + $exit_code = $repl->start(); + } while ( WP_CLI\Shell\REPL::EXIT_CODE_RESTART === $exit_code ); } } } diff --git a/src/WP_CLI/Shell/REPL.php b/src/WP_CLI/Shell/REPL.php index 15697bf0..b4665522 100644 --- a/src/WP_CLI/Shell/REPL.php +++ b/src/WP_CLI/Shell/REPL.php @@ -10,6 +10,8 @@ class REPL { private $history_file; + const EXIT_CODE_RESTART = 10; + public function __construct( $prompt ) { $this->prompt = $prompt; @@ -25,6 +27,12 @@ public function start() { continue; } + // Check for special restart command + if ( 'restart' === trim( $line ) ) { + WP_CLI::log( 'Restarting shell...' ); + return self::EXIT_CODE_RESTART; + } + $line = rtrim( $line, ';' ) . ';'; if ( self::starts_with( self::non_expressions(), $line ) ) { From f94d780299caeb983c84cbc8ba016304818df9d8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Oct 2025 12:26:28 +0000 Subject: [PATCH 3/7] Add --watch option for automatic shell restart on file changes Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- src/Shell_Command.php | 43 +++++++++++++++++++++++ src/WP_CLI/Shell/REPL.php | 73 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 116 insertions(+) diff --git a/src/Shell_Command.php b/src/Shell_Command.php index 00ec1103..38a2212e 100644 --- a/src/Shell_Command.php +++ b/src/Shell_Command.php @@ -19,6 +19,10 @@ class Shell_Command extends WP_CLI_Command { * : Force the use of WP-CLI's built-in PHP REPL, even if the Boris or * PsySH PHP REPLs are available. * + * [--watch=] + * : Watch a file or directory for changes and automatically restart the shell. + * Only works with the built-in REPL (--basic). + * * ## EXAMPLES * * # Call get_bloginfo() to get the name of the site. @@ -31,8 +35,21 @@ class Shell_Command extends WP_CLI_Command { * wp> restart * Restarting shell... * wp> + * + * # Watch a directory for changes and auto-restart. + * $ wp shell --watch=wp-content/plugins/my-plugin + * wp> // Make changes to files in the plugin directory + * Detected changes in wp-content/plugins/my-plugin, restarting shell... + * wp> */ public function __invoke( $_, $assoc_args ) { + $watch_path = Utils\get_flag_value( $assoc_args, 'watch', false ); + + if ( $watch_path && ! Utils\get_flag_value( $assoc_args, 'basic' ) ) { + WP_CLI::warning( 'The --watch option only works with the built-in REPL. Enabling --basic mode.' ); + $assoc_args['basic'] = true; + } + $class = WP_CLI\Shell\REPL::class; $implementations = array( @@ -61,10 +78,36 @@ public function __invoke( $_, $assoc_args ) { /** * @var class-string $class */ + if ( $watch_path ) { + $watch_path = $this->resolve_watch_path( $watch_path ); + } + do { $repl = new $class( 'wp> ' ); + if ( $watch_path ) { + $repl->set_watch_path( $watch_path ); + } $exit_code = $repl->start(); } while ( WP_CLI\Shell\REPL::EXIT_CODE_RESTART === $exit_code ); } } + + /** + * Resolve and validate the watch path. + * + * @param string $path Path to watch. + * @return string Absolute path to watch. + */ + private function resolve_watch_path( $path ) { + if ( ! file_exists( $path ) ) { + WP_CLI::error( "Watch path does not exist: {$path}" ); + } + + $realpath = realpath( $path ); + if ( false === $realpath ) { + WP_CLI::error( "Could not resolve watch path: {$path}" ); + } + + return $realpath; + } } diff --git a/src/WP_CLI/Shell/REPL.php b/src/WP_CLI/Shell/REPL.php index b4665522..bba89a23 100644 --- a/src/WP_CLI/Shell/REPL.php +++ b/src/WP_CLI/Shell/REPL.php @@ -10,6 +10,10 @@ class REPL { private $history_file; + private $watch_path; + + private $watch_mtime; + const EXIT_CODE_RESTART = 10; public function __construct( $prompt ) { @@ -18,15 +22,36 @@ public function __construct( $prompt ) { $this->set_history_file(); } + /** + * Set a path to watch for changes. + * + * @param string $path Path to watch for changes. + */ + public function set_watch_path( $path ) { + $this->watch_path = $path; + $this->watch_mtime = $this->get_recursive_mtime( $path ); + } + public function start() { // @phpstan-ignore while.alwaysTrue while ( true ) { + // Check for file changes if watching + if ( $this->watch_path && $this->has_changes() ) { + WP_CLI::log( "Detected changes in {$this->watch_path}, restarting shell..." ); + return self::EXIT_CODE_RESTART; + } + $line = $this->prompt(); if ( '' === $line ) { continue; } + // Check for special exit command + if ( 'exit' === trim( $line ) ) { + return 0; + } + // Check for special restart command if ( 'restart' === trim( $line ) ) { WP_CLI::log( 'Restarting shell...' ); @@ -161,4 +186,52 @@ private function set_history_file() { private static function starts_with( $tokens, $line ) { return preg_match( "/^($tokens)[\(\s]+/", $line ); } + + /** + * Check if the watched path has changes. + * + * @return bool True if changes detected, false otherwise. + */ + private function has_changes() { + if ( ! $this->watch_path ) { + return false; + } + + $current_mtime = $this->get_recursive_mtime( $this->watch_path ); + return $current_mtime !== $this->watch_mtime; + } + + /** + * Get the most recent modification time for a path recursively. + * + * @param string $path Path to check. + * @return int Most recent modification time. + */ + private function get_recursive_mtime( $path ) { + $mtime = 0; + + if ( is_file( $path ) ) { + $file_mtime = filemtime( $path ); + return false !== $file_mtime ? $file_mtime : 0; + } + + if ( is_dir( $path ) ) { + $dir_mtime = filemtime( $path ); + $mtime = false !== $dir_mtime ? $dir_mtime : 0; + + $iterator = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator( $path, \RecursiveDirectoryIterator::SKIP_DOTS ), + \RecursiveIteratorIterator::SELF_FIRST + ); + + foreach ( $iterator as $file ) { + $file_mtime = $file->getMTime(); + if ( $file_mtime > $mtime ) { + $mtime = $file_mtime; + } + } + } + + return $mtime; + } } From 371e60fef11ecfa864f358edd9746f8ed86c685b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Oct 2025 12:28:36 +0000 Subject: [PATCH 4/7] Add exit command and tests Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- features/shell.feature | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/features/shell.feature b/features/shell.feature index 5d1d9d54..de31c95b 100644 --- a/features/shell.feature +++ b/features/shell.feature @@ -79,3 +79,21 @@ Feature: WordPress REPL """ => int(2) """ + + Scenario: Exit shell + Given a WP install + And a session file: + """ + $a = 1; + exit + """ + + When I run `wp shell --basic < session` + Then STDOUT should contain: + """ + => int(1) + """ + And STDOUT should not contain: + """ + exit + """ From f502d3dbafa8e8c12b2717938ce28dc0bfa54311 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Oct 2025 15:25:53 +0000 Subject: [PATCH 5/7] Fix PHPCS alignment and PHPStan issues Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- src/WP_CLI/Shell/REPL.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/WP_CLI/Shell/REPL.php b/src/WP_CLI/Shell/REPL.php index bba89a23..501728e3 100644 --- a/src/WP_CLI/Shell/REPL.php +++ b/src/WP_CLI/Shell/REPL.php @@ -33,7 +33,6 @@ public function set_watch_path( $path ) { } public function start() { - // @phpstan-ignore while.alwaysTrue while ( true ) { // Check for file changes if watching if ( $this->watch_path && $this->has_changes() ) { @@ -216,8 +215,8 @@ private function get_recursive_mtime( $path ) { } if ( is_dir( $path ) ) { - $dir_mtime = filemtime( $path ); - $mtime = false !== $dir_mtime ? $dir_mtime : 0; + $dir_mtime = filemtime( $path ); + $mtime = false !== $dir_mtime ? $dir_mtime : 0; $iterator = new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator( $path, \RecursiveDirectoryIterator::SKIP_DOTS ), @@ -225,6 +224,7 @@ private function get_recursive_mtime( $path ) { ); foreach ( $iterator as $file ) { + /** @var \SplFileInfo $file */ $file_mtime = $file->getMTime(); if ( $file_mtime > $mtime ) { $mtime = $file_mtime; From 5e0157a226db46ae124e4a4584da84f7752f3835 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Thu, 30 Oct 2025 17:28:12 +0100 Subject: [PATCH 6/7] Fix alignment --- src/WP_CLI/Shell/REPL.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/WP_CLI/Shell/REPL.php b/src/WP_CLI/Shell/REPL.php index 501728e3..ace00064 100644 --- a/src/WP_CLI/Shell/REPL.php +++ b/src/WP_CLI/Shell/REPL.php @@ -28,7 +28,7 @@ public function __construct( $prompt ) { * @param string $path Path to watch for changes. */ public function set_watch_path( $path ) { - $this->watch_path = $path; + $this->watch_path = $path; $this->watch_mtime = $this->get_recursive_mtime( $path ); } From b5392748c8d6ac1b0c0f55a2f6000615aee2247c Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Thu, 30 Oct 2025 17:28:21 +0100 Subject: [PATCH 7/7] Fix alignment --- src/WP_CLI/Shell/REPL.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/WP_CLI/Shell/REPL.php b/src/WP_CLI/Shell/REPL.php index ace00064..418d15eb 100644 --- a/src/WP_CLI/Shell/REPL.php +++ b/src/WP_CLI/Shell/REPL.php @@ -215,8 +215,8 @@ private function get_recursive_mtime( $path ) { } if ( is_dir( $path ) ) { - $dir_mtime = filemtime( $path ); - $mtime = false !== $dir_mtime ? $dir_mtime : 0; + $dir_mtime = filemtime( $path ); + $mtime = false !== $dir_mtime ? $dir_mtime : 0; $iterator = new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator( $path, \RecursiveDirectoryIterator::SKIP_DOTS ),