Skip to content

Commit f42eaed

Browse files
committed
Add sites:wp-cli command to run WP-CLI commands from the CLI
1 parent aa63622 commit f42eaed

3 files changed

Lines changed: 185 additions & 0 deletions

File tree

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,12 @@ Nested properties should use dot notation, for example, `database.server`.
119119
# Start an SSH session as the site user
120120
spinupwp sites:ssh <site_id>
121121

122+
# Run a WP-CLI command on a site
123+
spinupwp sites:wp-cli <site_id> --command="core version"
124+
125+
# Run multiple WP-CLI commands on a site
126+
spinupwp sites:wp-cli <site_id> --command="core version" --command="plugin list --status=active"
127+
122128
You can pass any properties of the [Site Schema](https://api.spinupwp.com/?shell#tocS_Site) to the `--fields` flag.
123129
Nested properties should use dot notation, for example, `backups.next_run_time` or `git.branch`.
124130

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
<?php
2+
3+
namespace App\Commands\Sites;
4+
5+
use App\Commands\BaseCommand;
6+
7+
class WpCliCommand extends BaseCommand
8+
{
9+
protected $signature = 'sites:wp-cli
10+
{site_id? : The site to run WP-CLI commands on}
11+
{--command=* : The WP-CLI command(s) to run}
12+
{--profile= : The SpinupWP configuration profile to use}';
13+
14+
protected $description = 'Run WP-CLI commands on a site';
15+
16+
public function action(): int
17+
{
18+
$siteId = $this->argument('site_id');
19+
20+
if (empty($siteId)) {
21+
$siteId = $this->askToSelectSite('Which site would you like to run WP-CLI commands on');
22+
}
23+
24+
$commands = $this->option('command');
25+
26+
if (empty($commands)) {
27+
$commands = $this->askForCommands();
28+
}
29+
30+
if (empty($commands)) {
31+
$this->warn('No commands provided.');
32+
return self::SUCCESS;
33+
}
34+
35+
$eventId = $this->spinupwp->sites->wpCli((int) $siteId, $commands);
36+
37+
$this->successfulStep('WP-CLI commands queued for execution.');
38+
39+
$this->streamOutput($eventId);
40+
41+
return self::SUCCESS;
42+
}
43+
44+
protected function askForCommands(): array
45+
{
46+
$commands = [];
47+
48+
$this->step('Enter WP-CLI commands (empty line to finish):');
49+
50+
while (true) {
51+
$command = $this->ask('Command');
52+
53+
if (empty($command)) {
54+
break;
55+
}
56+
57+
$commands[] = $command;
58+
}
59+
60+
return $commands;
61+
}
62+
63+
protected function streamOutput(int $eventId): void
64+
{
65+
$displayedLength = 0;
66+
67+
while (true) {
68+
sleep(2);
69+
70+
$event = $this->spinupwp->events->get($eventId);
71+
$output = $event->output ?? '';
72+
73+
if (strlen($output) > $displayedLength) {
74+
$this->output->write(substr($output, $displayedLength));
75+
$displayedLength = strlen($output);
76+
}
77+
78+
if (in_array($event->status, ['deployed', 'failed'])) {
79+
if ($event->status === 'failed') {
80+
$this->error('WP-CLI command execution failed.');
81+
}
82+
83+
break;
84+
}
85+
}
86+
}
87+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
<?php
2+
3+
use GuzzleHttp\Psr7\Response;
4+
5+
beforeEach(function () {
6+
setTestConfigFile();
7+
8+
$this->clientMock->shouldReceive('request')->with('POST', 'sites/1/wp-cli', [
9+
'form_params' => [
10+
'commands' => ['core version'],
11+
],
12+
])->andReturn(
13+
new Response(200, [], json_encode(['event_id' => 100]))
14+
);
15+
});
16+
17+
afterEach(function () {
18+
deleteTestConfigFile();
19+
});
20+
21+
test('wp-cli command with site ID and command supplied', function () {
22+
$this->clientMock->shouldReceive('request')->with('GET', 'events/100', [])->andReturn(
23+
new Response(200, [], json_encode([
24+
'data' => [
25+
'id' => 100,
26+
'status' => 'deployed',
27+
'output' => "$ core version\n6.4.2",
28+
],
29+
]))
30+
);
31+
32+
$this->artisan('sites:wp-cli', ['site_id' => '1', '--command' => ['core version']])
33+
->expectsOutput('==> WP-CLI commands queued for execution.')
34+
->assertExitCode(0);
35+
});
36+
37+
test('wp-cli command with interactive site selection', function () {
38+
$this->clientMock->shouldReceive('request')->once()->with('GET', 'sites?page=1&limit=100', [])->andReturn(
39+
new Response(200, [], json_encode([
40+
'data' => [
41+
[
42+
'id' => 1,
43+
'domain' => 'hellfishmedia.com',
44+
],
45+
],
46+
'pagination' => [
47+
'previous' => null,
48+
'next' => null,
49+
'count' => 1,
50+
],
51+
]))
52+
);
53+
54+
$this->clientMock->shouldReceive('request')->with('GET', 'events/100', [])->andReturn(
55+
new Response(200, [], json_encode([
56+
'data' => [
57+
'id' => 100,
58+
'status' => 'deployed',
59+
'output' => "$ core version\n6.4.2",
60+
],
61+
]))
62+
);
63+
64+
$this->artisan('sites:wp-cli', ['--command' => ['core version']])
65+
->expectsQuestion('Which site would you like to run WP-CLI commands on', '1')
66+
->expectsOutput('==> WP-CLI commands queued for execution.')
67+
->assertExitCode(0);
68+
});
69+
70+
test('wp-cli command with multiple commands', function () {
71+
$this->clientMock->shouldReceive('request')->with('POST', 'sites/1/wp-cli', [
72+
'form_params' => [
73+
'commands' => ['core version', 'plugin list --status=active'],
74+
],
75+
])->andReturn(
76+
new Response(200, [], json_encode(['event_id' => 101]))
77+
);
78+
79+
$this->clientMock->shouldReceive('request')->with('GET', 'events/101', [])->andReturn(
80+
new Response(200, [], json_encode([
81+
'data' => [
82+
'id' => 101,
83+
'status' => 'deployed',
84+
'output' => "$ core version\n6.4.2\n\n$ plugin list --status=active\nName\tStatus\tVersion",
85+
],
86+
]))
87+
);
88+
89+
$this->artisan('sites:wp-cli', ['site_id' => '1', '--command' => ['core version', 'plugin list --status=active']])
90+
->expectsOutput('==> WP-CLI commands queued for execution.')
91+
->assertExitCode(0);
92+
});

0 commit comments

Comments
 (0)