From 6d7c8f825a33b0be6decf9adc5061730b56ae6ce Mon Sep 17 00:00:00 2001 From: Joe Tannenbaum Date: Fri, 10 Apr 2026 14:04:04 -0400 Subject: [PATCH 1/2] Update Tinker.php --- app/Commands/Tinker.php | 96 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 95 insertions(+), 1 deletion(-) diff --git a/app/Commands/Tinker.php b/app/Commands/Tinker.php index 24dbce4..f36b218 100644 --- a/app/Commands/Tinker.php +++ b/app/Commands/Tinker.php @@ -3,11 +3,13 @@ namespace App\Commands; use App\Client\Requests\CodeExecutionRequestData; +use App\Dto\CodeExecution; use App\Exceptions\CommandExitException; use Carbon\CarbonInterval; use Exception; use Illuminate\Foundation\Concerns\ResolvesDumpSource; use Illuminate\Support\Sleep; +use RuntimeException; use Saloon\Exceptions\Request\RequestException; use function Laravel\Prompts\error; @@ -23,8 +25,11 @@ class Tinker extends BaseCommand { use ResolvesDumpSource; + protected ?string $jsonDataClass = CodeExecution::class; + protected $signature = 'tinker {environment? : The environment ID or name} + {--code= : The code to execute} {--editor= : Open the code in the editor} {--timeout=60 : Maximum seconds to wait for output}'; @@ -34,6 +39,8 @@ class Tinker extends BaseCommand protected $tmpFileLastModifiedAt; + protected const POLL_INTERVAL_SECONDS = 2; + protected const RECENT_SAVE_WINDOW_SECONDS = 2; protected ?string $editorUrl = null; @@ -46,6 +53,10 @@ public function handle() $environment = $this->resolvers()->environment()->include('application')->from($this->argument('environment')); + if (! $this->isInteractive() || $this->option('code')) { + return $this->handleNonInteractively($environment); + } + $this->resolveEditorUrl(); if ($this->editorUrl) { @@ -109,7 +120,7 @@ public function handle() return $codeExecution; } - Sleep::for(CarbonInterval::second(2)); + Sleep::for(CarbonInterval::seconds(static::POLL_INTERVAL_SECONDS)); } }, 'Waiting for output...'); @@ -133,6 +144,89 @@ public function handle() } } + protected function handleNonInteractively($environment): int + { + $code = $this->option('code'); + + if (! $code) { + throw new RuntimeException('The --code option is required in non-interactive mode.'); + } + + try { + $codeExecution = $this->client->codeExecutions()->create( + new CodeExecutionRequestData( + environmentId: $environment->id, + code: $code, + ), + ); + } catch (RequestException $e) { + if ($e->getResponse()->status() === 422) { + $errors = $e->getResponse()->json('errors', []); + $message = ! empty($errors) + ? collect($errors)->map(fn ($msgs, $field) => ucwords($field).': '.implode(', ', $msgs))->implode('; ') + : $e->getResponse()->json('message', 'Validation error'); + + throw new RuntimeException($message); + } + + throw new RuntimeException($e->getMessage()); + } + + $codeExecution = $this->pollNonInteractively($codeExecution); + + $this->outputJsonIfWanted($codeExecution); + + if ($codeExecution->failureReason) { + $this->failAndExit($codeExecution->failureReason); + } + + if ($codeExecution->exitCode !== 0 && $codeExecution->exitCode !== null) { + if ($codeExecution->output) { + codeBlock($codeExecution->output, 'result'); + } + + $this->failAndExit('Code execution failed (exit code: '.$codeExecution->exitCode.').'); + } + + if ($codeExecution->output) { + codeBlock($codeExecution->output, 'result'); + } + + return self::SUCCESS; + } + + protected function pollNonInteractively(CodeExecution $codeExecution): CodeExecution + { + $startedAt = time(); + $timeout = (int) $this->option('timeout'); + $lastStatus = ''; + + while (true) { + if (time() - $startedAt >= $timeout) { + throw new RuntimeException('Code execution timed out.'); + } + + $codeExecution = $this->client->codeExecutions()->get($codeExecution->id); + + $currentStatus = $codeExecution->status->label(); + + if ($currentStatus !== $lastStatus) { + $this->writeJsonIfWanted([ + 'code_execution_id' => $codeExecution->id, + 'status' => $codeExecution->status->value, + 'message' => $currentStatus, + ]); + $lastStatus = $currentStatus; + } + + if ($codeExecution->output !== null) { + return $codeExecution; + } + + Sleep::for(CarbonInterval::seconds(static::POLL_INTERVAL_SECONDS)); + } + } + protected function resolveEditorUrl() { if ($this->input->getParameterOption('--editor', false) === false) { From fdc3a1a9af67de389130dd6726f84b252cb34efb Mon Sep 17 00:00:00 2001 From: Joe Tannenbaum Date: Fri, 10 Apr 2026 14:09:44 -0400 Subject: [PATCH 2/2] Update Tinker.php --- app/Commands/Tinker.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/Commands/Tinker.php b/app/Commands/Tinker.php index f36b218..9578969 100644 --- a/app/Commands/Tinker.php +++ b/app/Commands/Tinker.php @@ -172,7 +172,14 @@ protected function handleNonInteractively($environment): int throw new RuntimeException($e->getMessage()); } - $codeExecution = $this->pollNonInteractively($codeExecution); + if ($this->wantsJson()) { + $codeExecution = $this->pollNonInteractively($codeExecution); + } else { + $codeExecution = spin( + fn () => $this->pollNonInteractively($codeExecution), + 'Waiting for output...', + ); + } $this->outputJsonIfWanted($codeExecution);