Skip to content

Commit e985f98

Browse files
committed
Add Tinker Command
1 parent 2d8a584 commit e985f98

File tree

6 files changed

+223
-3
lines changed

6 files changed

+223
-3
lines changed

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"amphp/http-client": "^5.3",
2020
"amphp/process": "^2.0",
2121
"aws/aws-sdk-php": "^3.319",
22+
"psy/psysh": "^0.12.0",
2223
"revolt/event-loop": "^1.0",
2324
"symfony/console": "^5.2 || ^6.2 || ^7",
2425
"symfony/filesystem": "^5.2 || ^6.2 || ^7",

src/Application.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ public function __construct()
2828
$this->add(new Commands\Connect);
2929
$this->add(new Commands\PreviousLogs);
3030
$this->add(new Commands\Cloud);
31+
$this->add(new Commands\Tinker);
3132
}
3233

3334
public function doRun(InputInterface $input, OutputInterface $output): int
@@ -85,4 +86,4 @@ private function turnWarningsIntoExceptions(): void
8586
throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
8687
});
8788
}
88-
}
89+
}

src/Commands/Command.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
5050

5151
if ($invocation['status'] === 'success') {
5252
IO::spinClear();
53-
IO::writeln($invocation['output']);
53+
$output->writeln($invocation['output']);
5454
return 0;
5555
}
5656

@@ -102,4 +102,4 @@ private function writeErrorDetails(string $output): void
102102
IO::writeln(Styles::red($output));
103103
}
104104
}
105-
}
105+
}

src/Commands/Tinker.php

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php
2+
3+
namespace Bref\Cli\Commands;
4+
5+
use Bref\Cli\Tinker\BrefTinkerShell;
6+
use Psy\Configuration;
7+
use Psy\Shell;
8+
use Bref\Cli\Cli\IO;
9+
use Bref\Cli\Cli\Styles;
10+
use Bref\Cli\BrefCloudClient;
11+
use Symfony\Component\Console\Input\InputInterface;
12+
use Symfony\Component\Console\Input\StringInput;
13+
use Symfony\Component\Console\Output\OutputInterface;
14+
15+
class Tinker extends ApplicationCommand
16+
{
17+
protected function configure(): void
18+
{
19+
ini_set('memory_limit', '512M');
20+
21+
$this
22+
->setName('tinker')
23+
->setDescription('Run a Tinker shell in the lambda');
24+
parent::configure();
25+
}
26+
27+
protected function execute(InputInterface $input, OutputInterface $output): int
28+
{
29+
IO::writeln([Styles::brefHeader(), '']);
30+
31+
[
32+
'appName' => $appName,
33+
'environmentName' => $environmentName,
34+
'team' => $team,
35+
] = $this->parseStandardOptions($input);
36+
37+
// Auto enable verbose to avoid verbose async listener in VerboseModeEnabler which will causes issue when executing multiple commands
38+
IO::enableVerbose();
39+
40+
$config = Configuration::fromInput($input);
41+
$shellOutput = $config->getOutput();
42+
$shellOutput->writeln(sprintf("Starting Interactive Shell Session for <string>[%s]</string> in the <string>[%s]</string> environment", Styles::green($appName), Styles::red($environmentName)));
43+
44+
$shell = new BrefTinkerShell($config, str_replace("tinker", "command", (string) $input));
45+
$shell->setRawOutput($shellOutput);
46+
47+
return $shell->run();
48+
}
49+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
<?php
2+
3+
namespace Bref\Cli\Tinker;
4+
5+
use Bref\Cli\Commands\Command;
6+
use GuzzleHttp\Exception\ClientException;
7+
use Psy\Exception\BreakException;
8+
use Psy\Exception\ThrowUpException;
9+
use Psy\ExecutionClosure;
10+
use Psy\ExecutionLoop\AbstractListener;
11+
use Psy\Shell;
12+
use Symfony\Component\Console\Input\ArgvInput;
13+
use Symfony\Component\Console\Input\StringInput;
14+
use Symfony\Component\Console\Output\BufferedOutput;
15+
16+
class BrefTinkerLoopListener extends AbstractListener
17+
{
18+
protected string $commandInput;
19+
20+
public function __construct(string $commandInput)
21+
{
22+
$this->commandInput = $commandInput;
23+
}
24+
25+
public static function isSupported(): bool
26+
{
27+
return true;
28+
}
29+
30+
/**
31+
* @param BrefTinkerShell $shell
32+
* @throws BreakException
33+
* @throws ThrowUpException
34+
*/
35+
public function onExecute(Shell $shell, string $code)
36+
{
37+
if ($code == '\Psy\Exception\BreakException::exitShell();') {
38+
return $code;
39+
}
40+
41+
$vars = $shell->getScopeVariables(false);
42+
$context = $vars['_context'] ?? base64_encode(serialize(["_" => null]));
43+
// Evaluate the current code buffer
44+
try {
45+
$command = new Command();
46+
$args = join(" ", [
47+
'bref:tinker',
48+
'--execute=\"'.base64_encode($code).'\"',
49+
'--context=\"'.$context.'\"',
50+
]);
51+
$output = new BufferedOutput();
52+
$input = new ArgvInput(array_merge((new StringInput($this->commandInput))->getRawTokens(), [$args]));
53+
54+
$resultCode = $command->run($input, $output);
55+
$resultOutput = $output->fetch();
56+
if ($resultCode !== 0) {
57+
throw new BreakException("The remote tinker shell returned an error (code $resultCode).");
58+
}
59+
60+
$extractedOutput = $shell->extractContextData($resultOutput);
61+
if (is_null($extractedOutput)) {
62+
$shell->rawOutput->writeln(' <info> INFO </info> Please upgrade <string>laravel-bridge</string> package to latest version.');
63+
throw new BreakException("The remote tinker shell returned an invalid payload");
64+
}
65+
66+
if ([$output, $context, $return] = $extractedOutput) {
67+
if (!empty($output)) {
68+
$shell->rawOutput->writeln($output);
69+
}
70+
if (!empty($return)) {
71+
$shell->rawOutput->writeln($return);
72+
}
73+
if (!empty($context)) {
74+
// Extract _context into shell's scope variables for next code execution
75+
// Return NoValue as output and return value were printed out
76+
return "extract(['_context' => '{$context}']); return new \Psy\CodeCleaner\NoReturnValue();";
77+
} else {
78+
// Return NoValue as output and return value were printed out
79+
return "return new \Psy\CodeCleaner\NoReturnValue();";
80+
}
81+
}
82+
83+
return ExecutionClosure::NOOP_INPUT;
84+
} catch (ClientException $_e) {
85+
throw new BreakException($_e->getMessage());
86+
} catch (BreakException $breakException) {
87+
throw $breakException;
88+
} catch (\Throwable $throwable) {
89+
throw new ThrowUpException($throwable);
90+
}
91+
}
92+
}

src/Tinker/BrefTinkerShell.php

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<?php
2+
3+
namespace Bref\Cli\Tinker;
4+
5+
use Psy\Configuration;
6+
use Psy\Output\ShellOutput;
7+
use Psy\Shell;
8+
9+
class BrefTinkerShell extends Shell
10+
{
11+
/**
12+
* @var ShellOutput
13+
*/
14+
public $rawOutput;
15+
16+
protected string $commandInput;
17+
18+
public function __construct(?Configuration $config = null, string $commandInput = '')
19+
{
20+
$this->commandInput = $commandInput;
21+
22+
parent::__construct($config);
23+
}
24+
25+
public function setRawOutput($rawOutput)
26+
{
27+
$this->rawOutput = $rawOutput;
28+
29+
return $this;
30+
}
31+
32+
/**
33+
* Gets the default command loop listeners.
34+
*
35+
* @return array An array of Execution Loop Listener instances
36+
*/
37+
protected function getDefaultLoopListeners(): array
38+
{
39+
$listeners = parent::getDefaultLoopListeners();
40+
41+
$listeners[] = new BrefTinkerLoopListener($this->commandInput);
42+
43+
return $listeners;
44+
}
45+
46+
/**
47+
* @return list<string>|null
48+
*/
49+
public function extractContextData(string $output): ?array
50+
{
51+
$output = trim($output);
52+
// First, extract RETURN section if it exists
53+
if (preg_match('/\[RETURN\](.*?)\[END_RETURN\]/s', $output, $returnMatches)) {
54+
$returnValue = $returnMatches[1];
55+
// Remove RETURN section to work with the rest
56+
$output = (string) preg_replace('/\[RETURN\].*?\[END_RETURN\]/s', '', $output);
57+
} else {
58+
$returnValue = '';
59+
}
60+
61+
// Then extract CONTEXT section if it exists
62+
if (preg_match('/\[CONTEXT\](.*?)\[END_CONTEXT\]/s', $output, $contextMatches)) {
63+
$context = $contextMatches[1];
64+
// Remove CONTEXT section to get the before part
65+
$output = (string) preg_replace('/\[CONTEXT\].*?\[END_CONTEXT\]\n?/s', '', $output);
66+
} else {
67+
$context = '';
68+
}
69+
70+
// Only return null if we couldn't find any meaningful structure
71+
if (empty($output) && empty($context) && empty($returnValue)) {
72+
return null;
73+
}
74+
75+
return [$output, $context, $returnValue];
76+
}
77+
}

0 commit comments

Comments
 (0)