diff --git a/features/package-install.feature b/features/package-install.feature index cafd1e84..5a5d5c81 100644 --- a/features/package-install.feature +++ b/features/package-install.feature @@ -1262,3 +1262,20 @@ Feature: Install WP-CLI packages Error: ZipArchive failed to unzip 'package-dir/zero.zip': Not a zip archive (19). """ And STDOUT should be empty + + @github-api + Scenario: Install package with --no-interaction fails fast on Git authentication errors + Given an empty directory + + # Try to install from a repository that requires authentication + # With --no-interaction and GIT_TERMINAL_PROMPT=0, Git will fail immediately + # instead of prompting for credentials + When I try `wp package install git@github.com:wp-cli-private-test/authentication-required.git --no-interaction` + Then the return code should be 1 + # The command should fail fast without hanging + And STDERR should contain: + """ + Package installation failed + """ + # Git should report it couldn't authenticate, not prompt + And STDERR should match /fatal:|Could not read from remote repository|Repository not found/ diff --git a/features/package-update.feature b/features/package-update.feature index afeec92c..6d36ea89 100644 --- a/features/package-update.feature +++ b/features/package-update.feature @@ -194,3 +194,22 @@ Feature: Update WP-CLI packages Error: Package 'non-existent/package' is not installed. """ And the return code should be 1 + + @github-api + Scenario: Update packages with --no-interaction completes without prompting + Given an empty directory + + # Install a real package + When I run `wp package install danielbachhuber/wp-cli-reset-post-date-command` + Then STDOUT should contain: + """ + Success: Package installed. + """ + + # Update with --no-interaction should complete without hanging + When I run `wp package update --no-interaction` + Then STDOUT should contain: + """ + Packages updated. + """ + And STDERR should be empty diff --git a/features/package.feature b/features/package.feature index 4406477b..74861450 100644 --- a/features/package.feature +++ b/features/package.feature @@ -209,6 +209,25 @@ Feature: Manage WP-CLI packages {NO_SUCH_PACKAGE_COMPOSER_JSON} """ + @github-api + Scenario: Uninstall a package with --no-interaction prevents Git credential prompts + Given an empty directory + + # Install a real package first + When I run `wp package install danielbachhuber/wp-cli-reset-post-date-command` + Then STDOUT should contain: + """ + Success: Package installed. + """ + + # Uninstall with --no-interaction should complete without hanging + When I run `wp package uninstall danielbachhuber/wp-cli-reset-post-date-command --no-interaction` + Then STDERR should be empty + And STDOUT should contain: + """ + Success: Uninstalled package. + """ + Scenario: List packages with --skip-update-check flag Given an empty directory diff --git a/src/Package_Command.php b/src/Package_Command.php index b3eae9d4..6c39ed02 100644 --- a/src/Package_Command.php +++ b/src/Package_Command.php @@ -198,6 +198,9 @@ public function browse( $_, $assoc_args ) { * [--insecure] * : Retry downloads without certificate validation if TLS handshake fails. Note: This makes the request vulnerable to a MITM attack. * + * [--interaction] + * : Control interactive mode. Use `--no-interaction` to disable prompts (interactive by default). Useful for scripting. + * * ## EXAMPLES * * # Install a package hosted at a git URL. @@ -215,7 +218,12 @@ public function browse( $_, $assoc_args ) { public function install( $args, $assoc_args ) { list( $package_name ) = $args; - $insecure = (bool) Utils\get_flag_value( $assoc_args, 'insecure', false ); + $insecure = (bool) Utils\get_flag_value( $assoc_args, 'insecure', false ); + $interaction = (bool) Utils\get_flag_value( $assoc_args, 'interaction', true ); + + if ( ! $interaction ) { + $this->set_non_interactive_mode(); + } $this->set_composer_auth_env_var(); $git_package = false; @@ -624,6 +632,9 @@ public function get( $args, $assoc_args ) { * [...] * : One or more package names to update. If not specified, all packages will be updated. * + * [--interaction] + * : Control interactive mode. Use `--no-interaction` to disable prompts (interactive by default). Useful for scripting. + * * ## EXAMPLES * * # Update all packages. @@ -651,8 +662,17 @@ public function get( $args, $assoc_args ) { * Generating autoload files * --- * Success: Package updated successfully. + * + * @param array $args Positional arguments. One or more package names to update. + * @param array{interaction?: bool} $assoc_args Associative arguments. */ - public function update( $args = [] ) { + public function update( $args, $assoc_args = [] ) { + $interaction = (bool) Utils\get_flag_value( $assoc_args, 'interaction', true ); + + if ( ! $interaction ) { + $this->set_non_interactive_mode(); + } + $this->set_composer_auth_env_var(); // Validate package names if provided @@ -749,6 +769,9 @@ function ( $event ) use ( &$updated_packages ) { * [--insecure] * : Retry downloads without certificate validation if TLS handshake fails. Note: This makes the request vulnerable to a MITM attack. * + * [--interaction] + * : Control interactive prompts. Use `--no-interaction` to disable interactive questions (useful for scripting). + * * ## EXAMPLES * * # Uninstall package. @@ -761,7 +784,12 @@ function ( $event ) use ( &$updated_packages ) { public function uninstall( $args, $assoc_args ) { list( $package_name ) = $args; - $insecure = (bool) Utils\get_flag_value( $assoc_args, 'insecure', false ); + $insecure = (bool) Utils\get_flag_value( $assoc_args, 'insecure', false ); + $interaction = (bool) Utils\get_flag_value( $assoc_args, 'interaction', true ); + + if ( ! $interaction ) { + $this->set_non_interactive_mode(); + } $this->set_composer_auth_env_var(); $package = $this->get_installed_package_by_name( $package_name ); @@ -1645,4 +1673,21 @@ private function get_github_default_branch( $package_name, $insecure = false ) { return $default_branch; } + + /** + * Sets environment variables to enable non-interactive mode. + * + * This prevents Git from prompting for credentials (e.g., SSH passwords), + * which is useful for scripting and automation. + * + * Note: This uses putenv() which affects the entire PHP process, including + * any Git operations spawned by Composer. This is intentional to ensure + * non-interactive behavior propagates to all child processes. + */ + private function set_non_interactive_mode() { + // Prevent Git from prompting for credentials + putenv( 'GIT_TERMINAL_PROMPT=0' ); + // Prevent SSH from prompting for passwords + putenv( 'GIT_SSH_COMMAND=ssh -o BatchMode=yes' ); + } }