RVCFE3CZXAR4W7BCMGCZ2CRNWRFZFYDY7UO2EZGRBAJF6NFYGOVAC WHWKBNW24LBD5IU7B2CG53XZ6MXLOXTHKINIUZZHNVJUFVHGBKPQC 32ICZXL46QYHSFGB4PQGZMUVMQNR5N4UFZ45WPMVGL5PNBFNXK2QC KGTS53ADNL4QQG4F5TSJ5RWKY7CC6A7DC47LAXIFTBSYO7XS754AC ZHWYHIOHU224GOPHKYQK2I3M2MIHRNIK2B5BGYRQTLQ6CPZ7HM2QC F435FWSFVVX77ADD2R27X3TTECEYWAVV7X6MDLMMTGBJZBCRMMFQC WNLMHTPQCWCZD7URTMIATDAT2HJZJPF77C2CN22S43DRKCHUI3HAC QFA6RRE2GAYSHZTMAW54OC7AZGUJA6BCCCPE6BW3OL7B4EQQW5AQC #!/usr/bin/env php<?phpdefine('LARAVEL_START', microtime(true));/*|--------------------------------------------------------------------------| Register The Auto Loader|--------------------------------------------------------------------------|| Composer provides a convenient, automatically generated class loader| for our application. We just need to utilize it! We'll require it| into the script here so that we do not have to worry about the| loading of any our classes "manually". Feels great to relax.|*/$autoloader = require file_exists(__DIR__.'/vendor/autoload.php') ? __DIR__.'/vendor/autoload.php' : __DIR__.'/../../autoload.php';$app = require_once __DIR__.'/bootstrap/app.php';/*|--------------------------------------------------------------------------| Run The Artisan Application|--------------------------------------------------------------------------|| When we run the console application, the current CLI command will be| executed in this console and the response sent back to a terminal| or another output device for the developers. Here goes nothing!|*/$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);$status = $kernel->handle($input = new Symfony\Component\Console\Input\ArgvInput,new Symfony\Component\Console\Output\ConsoleOutput);/*|--------------------------------------------------------------------------| Shutdown The Application|--------------------------------------------------------------------------|| Once Artisan has finished running, we will fire off the shutdown events| so that any final work may be done by the application before we shut| down the process. This is the last thing to happen to the request.|*/$kernel->terminate($input, $status);exit($status);
{"name": "laravel-zero/laravel-zero","description": "The Laravel Zero Framework.","keywords": ["framework", "laravel", "laravel zero", "console", "cli"],"homepage": "https://laravel-zero.com","type": "project","license": "MIT","support": {"issues": "https://github.com/laravel-zero/laravel-zero/issues","source": "https://github.com/laravel-zero/laravel-zero"},"authors": [{"name": "Nuno Maduro","email": "enunomaduro@gmail.com"}],"require": {"php": "^8.2","laravel-zero/framework": "^12.0.2"},"require-dev": {"laravel/pint": "^1.25.1","mockery/mockery": "^1.6.12","pestphp/pest": "^3.8.4|^4.1.2"},"autoload": {"psr-4": {"App\\": "app/","Database\\Factories\\": "database/factories/","Database\\Seeders\\": "database/seeders/"}},"autoload-dev": {"psr-4": {"Tests\\": "tests/"}},"config": {"preferred-install": "dist","sort-packages": true,"optimize-autoloader": true,"allow-plugins": {"pestphp/pest-plugin": true}},"minimum-stability": "stable","prefer-stable": true,}"bin": ["pijultester"]
return match ($process->command) {['pijul', 'log', '--output-format', 'json', '--limit', '500'] => Process::result(json_encode([
return match (true) {$process->command === ['pijul', 'log', '--output-format', 'json', '--limit', '500'] => Process::result(json_encode([
['pijul', 'record', '-m', 'feat(pijul-qol): add laravel command', 'app'] => Process::result(''),
$process->command === ['pijul', 'record', 'app']&& ($process->environment['ANI_PIJUL_EDITOR_MODE'] ?? null) === 'capture' => tap(Process::result('', 'Captured Pijul hunks.', 1),function () use ($process): void {file_put_contents((string) $process->environment['ANI_PIJUL_CAPTURE_PATH'],<<<'TEXT'message = ""# Hunks1. Edit in "README.md"+ README change2. Edit in "app/Console.php"+ Console changeTEXT);},),$process->command === ['pijul', 'record', 'app']&& ($process->environment['ANI_PIJUL_EDITOR_MODE'] ?? null) === 'apply'&& ($process->environment['ANI_PIJUL_MESSAGE'] ?? null) === 'feat(pijul-qol): add laravel command'&& ($process->environment['ANI_PIJUL_SELECTED_HUNKS'] ?? null) === '[1]' => Process::result(''),
Process::assertRanTimes(fn (PendingProcess $process) => $process->command === ['pijul', 'record', 'app'], 2);Process::assertRan(fn (PendingProcess $process) => $process->command === ['pijul', 'record', 'app']&& ($process->environment['ANI_PIJUL_EDITOR_MODE'] ?? null) === 'capture'&& ! str_contains((string) ($process->environment['EDITOR'] ?? ''), "'")&& ! $process->tty&& $process->timeout === null);Process::assertRan(fn (PendingProcess $process) => $process->command === ['pijul', 'record', 'app']&& ($process->environment['ANI_PIJUL_EDITOR_MODE'] ?? null) === 'apply'&& ! str_contains((string) ($process->environment['EDITOR'] ?? ''), "'")&& ! $process->tty&& $process->timeout === null);
return match ($process->command) {['pijul', 'log', '--output-format', 'json', '--limit', '500'] => Process::result(json_encode([
return match (true) {$process->command === ['pijul', 'log', '--output-format', 'json', '--limit', '500'] => Process::result(json_encode([
['pijul', 'record', '-m', 'ship it', '--all', '--description', 'desc', 'app'] => Process::result(''),
$process->command === ['pijul', 'record', '--all', '--description', 'desc', 'app']&& ($process->environment['ANI_PIJUL_EDITOR_MODE'] ?? null) === 'capture' => tap(Process::result('', 'Captured Pijul hunks.', 1),function () use ($process): void {file_put_contents((string) $process->environment['ANI_PIJUL_CAPTURE_PATH'],<<<'TEXT'message = ""# Hunks1. Edit in "README.md"+ README changeTEXT);},),$process->command === ['pijul', 'record', '--all', '--description', 'desc', 'app']&& ($process->environment['ANI_PIJUL_EDITOR_MODE'] ?? null) === 'apply'&& ($process->environment['ANI_PIJUL_MESSAGE'] ?? null) === 'ship it'&& ($process->environment['ANI_PIJUL_SELECTED_HUNKS'] ?? null) === '[1]' => Process::result(''),
Process::assertRanTimes(fn (PendingProcess $process) => $process->command === ['pijul', 'record', '--all', '--description', 'desc', 'app'], 2);Process::assertRan(fn (PendingProcess $process) => $process->command === ['pijul', 'record', '--all', '--description', 'desc', 'app']&& ($process->environment['ANI_PIJUL_EDITOR_MODE'] ?? null) === 'capture'&& ! str_contains((string) ($process->environment['EDITOR'] ?? ''), "'")&& ! $process->tty&& $process->timeout === null);Process::assertRan(fn (PendingProcess $process) => $process->command === ['pijul', 'record', '--all', '--description', 'desc', 'app']&& ($process->environment['ANI_PIJUL_EDITOR_MODE'] ?? null) === 'apply'&& ! str_contains((string) ($process->environment['EDITOR'] ?? ''), "'")&& ! $process->tty&& $process->timeout === null);
it('still forwards the message directly when the command runs non-interactively', function () {Process::fake(function (PendingProcess $process) {return match ($process->command) {['pijul', 'log', '--output-format', 'json', '--limit', '500'] => Process::result(json_encode([['message' => 'feat(pijul-qol): first'],], JSON_THROW_ON_ERROR)),['pijul', 'record', '-m', 'ship it', '--all', '--description', 'desc', 'app'] => Process::result(''),default => Process::result('', 'Unexpected command.', 1),};});$kernel = app(\Illuminate\Contracts\Console\Kernel::class);$artisan = new ReflectionMethod($kernel, 'getArtisan');$artisan->setAccessible(true);$command = $artisan->invoke($kernel)->find('pijul:record-with-intent-tags');$input = new Symfony\Component\Console\Input\ArrayInput(['command' => 'pijul:record-with-intent-tags','--message' => 'ship it','--all' => true,'--description' => 'desc','prefixes' => ['app'],]);$input->setInteractive(false);$exitCode = $command->run($input, new Symfony\Component\Console\Output\BufferedOutput);expect($exitCode)->toBe(0);Process::assertRan(fn (PendingProcess $process) => $process->command === ['pijul', 'record', '-m', 'ship it', '--all', '--description', 'desc', 'app']&& $process->environment === []&& ! $process->tty);});
<?phpreturn [/*|--------------------------------------------------------------------------| Application Name|--------------------------------------------------------------------------|| This value is the name of your application. This value is used when the| framework needs to place the application's name in a notification or| any other location as required by the application or its packages.|*//*|--------------------------------------------------------------------------| Application Version|--------------------------------------------------------------------------|| This value determines the "version" your application is currently running| in. You may want to follow the "Semantic Versioning" - Given a version| number MAJOR.MINOR.PATCH when an update happens: https://semver.org.|*/'version' => app('git.version'),/*|--------------------------------------------------------------------------| Application Environment|--------------------------------------------------------------------------|| This value determines the "environment" your application is currently| running in. This may determine how you prefer to configure various| services the application utilizes. This can be overridden using| the global command line "--env" option when calling commands.|*/'env' => 'development',/*|--------------------------------------------------------------------------| Autoloaded Service Providers|--------------------------------------------------------------------------|| The service providers listed here will be automatically loaded on the| request to your application. Feel free to add your own services to| this array to grant expanded functionality to your applications.|*/'providers' => [App\Providers\AppServiceProvider::class,],];'name' => 'Pijultester',
<?phpreturn [/*|--------------------------------------------------------------------------| Application Name|--------------------------------------------------------------------------|| This value is the name of your application. This value is used when the| framework needs to place the application's name in a notification or| any other location as required by the application or its packages.|*/'name' => 'Ani',/*|--------------------------------------------------------------------------| Application Version|--------------------------------------------------------------------------|| This value determines the "version" your application is currently running| in. You may want to follow the "Semantic Versioning" - Given a version| number MAJOR.MINOR.PATCH when an update happens: https://semver.org.|*/'version' => app('git.version'),/*|--------------------------------------------------------------------------| Application Environment|--------------------------------------------------------------------------|| This value determines the "environment" your application is currently| running in. This may determine how you prefer to configure various| services the application utilizes. This can be overridden using| the global command line "--env" option when calling commands.|*/'env' => 'development',/*|--------------------------------------------------------------------------| Autoloaded Service Providers|--------------------------------------------------------------------------|| The service providers listed here will be automatically loaded on the| request to your application. Feel free to add your own services to| this array to grant expanded functionality to your applications.|*/'providers' => [App\Providers\AppServiceProvider::class,],];
{"name": "laravel-zero/laravel-zero","description": "The Laravel Zero Framework.","keywords": ["framework", "laravel", "laravel zero", "console", "cli"],"homepage": "https://laravel-zero.com","type": "project","license": "MIT","support": {"issues": "https://github.com/laravel-zero/laravel-zero/issues","source": "https://github.com/laravel-zero/laravel-zero"},"authors": [{"name": "Nuno Maduro","email": "enunomaduro@gmail.com"}],"require": {"php": "^8.2","laravel-zero/framework": "^12.0.2"},"require-dev": {"laravel/pint": "^1.25.1","mockery/mockery": "^1.6.12","pestphp/pest": "^3.8.4|^4.1.2"},"autoload": {"psr-4": {"App\\": "app/","Database\\Factories\\": "database/factories/","Database\\Seeders\\": "database/seeders/"}},"autoload-dev": {"psr-4": {"Tests\\": "tests/"}},"config": {"preferred-install": "dist","sort-packages": true,"optimize-autoloader": true,"allow-plugins": {"pestphp/pest-plugin": true}},"minimum-stability": "stable","prefer-stable": true,"bin": ["ani"]}
}private function recordChange(string $message): void{if (! $this->input->isInteractive()) {$this->runPijul($this->recordArguments($message));return;}$hunks = $this->extractRecordHunks($this->captureRecordTemplate());$selectedHunks = $this->promptForHunkSelection($hunks);$editorScript = $this->createRecordEditorScript();try {$editorCommand = $this->recordEditorCommand($editorScript);$this->runPijul($this->recordArguments(),environment: ['EDITOR' => $editorCommand,'VISUAL' => $editorCommand,'ANI_PIJUL_EDITOR_MODE' => 'apply','ANI_PIJUL_MESSAGE' => $message,'ANI_PIJUL_SELECTED_HUNKS' => json_encode($selectedHunks, JSON_THROW_ON_ERROR),],disableTimeout: true,);} finally {unlink($editorScript);}}/*** @param list<string> $tags*/private function promptForTag(array $tags): string{if ($tags === []) {return '';}return trim((string) (new LiveSuggestPrompt(label: 'Intent tag (optional)',options: fn (string $value): array => $this->suggestedTags($tags, $value),placeholder: 'Type to filter known tags, or press Enter to skip',scroll: self::PROMPT_SCROLL,hint: 'Results update as you type. Use arrows to choose, or press Enter on an empty prompt to skip.',))->prompt());}private function captureRecordTemplate(): string{$editorScript = $this->createRecordEditorScript();$capturePath = tempnam(sys_get_temp_dir(), 'ani-pijul-capture-');if ($capturePath === false) {unlink($editorScript);throw new RuntimeException('Unable to create a temporary file for capturing Pijul hunks.');}try {$result = $this->tryPijul($this->recordArguments(),environment: ['EDITOR' => $this->recordEditorCommand($editorScript),'VISUAL' => $this->recordEditorCommand($editorScript),'ANI_PIJUL_EDITOR_MODE' => 'capture','ANI_PIJUL_CAPTURE_PATH' => $capturePath,],disableTimeout: true,);$template = file_get_contents($capturePath);if (is_string($template) && $template !== '') {return $template;}$this->ensurePijulSucceeded($result);} finally {unlink($editorScript);if (is_file($capturePath)) {unlink($capturePath);}}throw new RuntimeException('Unable to capture Pijul hunks.');}/*** @return list<array{number:int,label:string}>*/private function extractRecordHunks(string $template): array{preg_match_all('/(?ms)^(?<number>\d+)\.\s.*?(?=^\d+\.\s|\z)/', $template, $matches, PREG_SET_ORDER);return array_map(function (array $match): array {$lines = preg_split('/\R/', trim($match[0])) ?: [];$headline = trim((string) array_shift($lines));$detail = '';foreach ($lines as $line) {$line = trim($line);if ($line === '' || str_starts_with($line, 'up ')) {continue;}$detail = $line;break;}return ['number' => (int) $match['number'],'label' => $detail === ''? $headline: sprintf('%s - %s', $headline, $detail),];}, $matches);}/*** @param list<array{number:int,label:string}> $hunks* @return list<int>*/private function promptForHunkSelection(array $hunks): array{if ($hunks === []) {throw new RuntimeException('No Pijul hunks were found to record.');}$options = [];foreach ($hunks as $hunk) {$options[(string) $hunk['number']] = $hunk['label'];}return array_map(static fn (string $value): int => (int) $value,(new MultiSelectPrompt(label: 'Select hunks to record',options: $options,default: count($options) === 1 ? array_keys($options) : [],scroll: self::PROMPT_SCROLL,required: 'Select at least one hunk to record.',hint: 'Use space to toggle hunks, Ctrl+A to toggle all, and Enter to continue.',))->prompt(),);}/*** @param list<string> $tags* @return list<string>*/private function suggestedTags(array $tags, string $query): array{$query = trim($query);if ($query === '') {return array_values($tags);}return array_values(array_filter($tags,fn (string $tag): bool => str_contains(strtolower($tag), strtolower($query)),));}private function createRecordEditorScript(): string{$path = tempnam(sys_get_temp_dir(), 'ani-pijul-editor-');if ($path === false) {throw new RuntimeException('Unable to create a temporary Pijul editor helper.');}$script = <<<'PHP'<?php$file = $argv[1] ?? null;if (! is_string($file) || $file === '') {fwrite(STDERR, "Missing Pijul editor file.\n");exit(1);}$mode = getenv('ANI_PIJUL_EDITOR_MODE');$template = file_get_contents($file);if ($template === false) {fwrite(STDERR, "Unable to read the Pijul editor file.\n");exit(1);}if ($mode === 'capture') {$capturePath = getenv('ANI_PIJUL_CAPTURE_PATH');if (! is_string($capturePath) || $capturePath === '') {fwrite(STDERR, "Missing ANI_PIJUL_CAPTURE_PATH.\n");exit(1);}if (file_put_contents($capturePath, $template) === false) {fwrite(STDERR, "Unable to capture the Pijul editor file.\n");exit(1);}fwrite(STDERR, "Captured Pijul hunks.\n");exit(1);}if ($mode !== 'apply') {fwrite(STDERR, "Unsupported ANI_PIJUL_EDITOR_MODE.\n");exit(1);}$message = getenv('ANI_PIJUL_MESSAGE');if (! is_string($message)) {fwrite(STDERR, "Missing ANI_PIJUL_MESSAGE.\n");exit(1);}$encodedMessage = json_encode($message, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);if (! is_string($encodedMessage)) {fwrite(STDERR, "Unable to encode the Pijul change message.\n");exit(1);}$updated = preg_replace('/^message = .*$/m', 'message = '.$encodedMessage, $template, 1, $count);if (! is_string($updated) || $count !== 1) {fwrite(STDERR, "Unable to prefill the Pijul change message.\n");exit(1);}$selectedHunks = json_decode((string) getenv('ANI_PIJUL_SELECTED_HUNKS'), true);if (! is_array($selectedHunks)) {fwrite(STDERR, "Missing ANI_PIJUL_SELECTED_HUNKS.\n");exit(1);}$selected = [];foreach ($selectedHunks as $value) {if (is_int($value) || (is_string($value) && ctype_digit($value))) {$selected[(string) $value] = true;}}if ($selected === []) {fwrite(STDERR, "No hunks were selected.\n");exit(1);}if (preg_match('/^\d+\.\s/m', $updated, $firstHunk, PREG_OFFSET_CAPTURE) !== 1) {fwrite(STDERR, "Unable to find hunks in the Pijul editor file.\n");exit(1);}$header = rtrim(substr($updated, 0, $firstHunk[0][1]), "\n");$body = substr($updated, $firstHunk[0][1]);if (preg_match_all('/(?ms)^\d+\.\s.*?(?=^\d+\.\s|\z)/', $body, $matches) !== false) {$blocks = [];foreach ($matches[0] as $block) {if (preg_match('/^(?<number>\d+)\.\s/m', $block, $numberMatch) !== 1) {continue;}if (isset($selected[$numberMatch['number']])) {$blocks[] = rtrim($block, "\n");}}if ($blocks === []) {fwrite(STDERR, "None of the selected hunks matched the Pijul editor file.\n");exit(1);}$updated = $header."\n\n".implode("\n\n", $blocks)."\n";}if (file_put_contents($file, $updated) === false) {fwrite(STDERR, "Unable to update the Pijul editor file.\n");exit(1);}PHP;if (file_put_contents($path, $script) === false) {unlink($path);throw new RuntimeException('Unable to write the temporary Pijul editor helper.');}if (! chmod($path, 0700)) {unlink($path);throw new RuntimeException('Unable to make the temporary Pijul editor helper executable.');}return $path;
protected function runPijul(array $arguments, bool $interactive = false): ProcessResult{
protected function runPijul(array $arguments,bool $interactive = false,array $environment = [],bool $disableTimeout = false,): ProcessResult {return $this->ensurePijulSucceeded($this->tryPijul($arguments, $interactive, $environment, $disableTimeout));}protected function tryPijul(array $arguments,bool $interactive = false,array $environment = [],bool $disableTimeout = false,): ProcessResult {
#!/usr/bin/env php<?phpdefine('LARAVEL_START', microtime(true));/*|--------------------------------------------------------------------------| Register The Auto Loader|--------------------------------------------------------------------------|| Composer provides a convenient, automatically generated class loader| for our application. We just need to utilize it! We'll require it| into the script here so that we do not have to worry about the| loading of any our classes "manually". Feels great to relax.|*/$autoloader = require file_exists(__DIR__.'/vendor/autoload.php') ? __DIR__.'/vendor/autoload.php' : __DIR__.'/../../autoload.php';$app = require_once __DIR__.'/bootstrap/app.php';/*|--------------------------------------------------------------------------| Run The Artisan Application|--------------------------------------------------------------------------|| When we run the console application, the current CLI command will be| executed in this console and the response sent back to a terminal| or another output device for the developers. Here goes nothing!|*/$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);$status = $kernel->handle($input = new Symfony\Component\Console\Input\ArgvInput,new Symfony\Component\Console\Output\ConsoleOutput);/*|--------------------------------------------------------------------------| Shutdown The Application|--------------------------------------------------------------------------|| Once Artisan has finished running, we will fire off the shutdown events| so that any final work may be done by the application before we shut| down the process. This is the last thing to happen to the request.|*/$kernel->terminate($input, $status);exit($status);
- The intent-tag prompt supports terminal autocompletion from recent `pijul log` messages.
- The intent-tag prompt uses the same live suggestion UI as the search command.- In an interactive terminal, the command now shows an explicit multi-select hunk picker before recording, so you can choose exactly which hunks to send through without editing Pijul's temp file by hand.
## Before-push review against a remotePijul does not have a direct equivalent to `git diff origin/main...HEAD`, so the easiest review workflow is to pull the remote channel into a local review channel and diff against that.Example:```bashpijul pull origin --from-channel main --to-channel review-mainpijul channel switch my-workpijul diff --channel review-main```