Skip to content

Commit 1a5bb31

Browse files
committed
Add AI agent detection to auto-switch to JSON output
1 parent a44d54e commit 1a5bb31

6 files changed

Lines changed: 213 additions & 4 deletions

File tree

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"nette/utils": "^3.2.5",
2525
"nikic/php-parser": "^5.7.0",
2626
"ondram/ci-detector": "^4.0",
27+
"shipfastlabs/agent-detector": "^1.0",
2728
"ondrejmirtes/better-reflection": "6.65.0.9",
2829
"ondrejmirtes/composer-attribute-collector": "^1.1.1",
2930
"ondrejmirtes/php-merge": "^4.1",

composer.lock

Lines changed: 69 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Command/AnalyseCommand.php

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use OndraM\CiDetector\CiDetector;
66
use Override;
77
use PHPStan\Analyser\InternalError;
8+
use PHPStan\Command\ErrorFormatter\AgentDetectedErrorFormatter;
89
use PHPStan\Command\ErrorFormatter\BaselineNeonErrorFormatter;
910
use PHPStan\Command\ErrorFormatter\BaselinePhpErrorFormatter;
1011
use PHPStan\Command\ErrorFormatter\ErrorFormatter;
@@ -235,11 +236,20 @@ protected function execute(InputInterface $input, OutputInterface $output): int
235236
$errorFormat = $inceptionResult->getContainer()->getParameter('errorFormat');
236237
}
237238

239+
$container = $inceptionResult->getContainer();
240+
241+
if ($errorFormat === null) {
242+
/** @var AgentDetectedErrorFormatter $agentFormatter */
243+
$agentFormatter = $container->getByType(AgentDetectedErrorFormatter::class);
244+
if ($agentFormatter->isAgentDetected()) {
245+
$errorFormat = 'json';
246+
}
247+
}
248+
238249
if ($errorFormat === null) {
239250
$errorFormat = 'table';
240251
}
241252

242-
$container = $inceptionResult->getContainer();
243253
$errorFormatterServiceName = sprintf('errorFormatter.%s', $errorFormat);
244254
if (!$container->hasService($errorFormatterServiceName)) {
245255
$errorOutput->writeLineFormatted(sprintf(
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Command\ErrorFormatter;
4+
5+
use AgentDetector\AgentDetector;
6+
use PHPStan\Command\AnalysisResult;
7+
use PHPStan\Command\Output;
8+
use PHPStan\DependencyInjection\AutowiredParameter;
9+
use PHPStan\DependencyInjection\AutowiredService;
10+
11+
/**
12+
* @api
13+
*/
14+
#[AutowiredService(as: AgentDetectedErrorFormatter::class)]
15+
final class AgentDetectedErrorFormatter implements ErrorFormatter
16+
{
17+
18+
public function __construct(
19+
#[AutowiredParameter(ref: '@errorFormatter.json')]
20+
private JsonErrorFormatter $jsonErrorFormatter,
21+
)
22+
{
23+
}
24+
25+
public function isAgentDetected(): bool
26+
{
27+
return AgentDetector::detect()->isAgent;
28+
}
29+
30+
public function formatErrors(AnalysisResult $analysisResult, Output $output): int
31+
{
32+
return $this->jsonErrorFormatter->formatErrors($analysisResult, $output);
33+
}
34+
35+
}

src/Command/ErrorsConsoleStyle.php

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace PHPStan\Command;
44

5+
use AgentDetector\AgentDetector;
56
use OndraM\CiDetector\CiDetector;
67
use Override;
78
use Symfony\Component\Console\Helper\Helper;
@@ -29,6 +30,8 @@ final class ErrorsConsoleStyle extends SymfonyStyle
2930

3031
private ?bool $isCiDetected = null;
3132

33+
private ?bool $isAgentDetected = null;
34+
3235
public function __construct(InputInterface $input, OutputInterface $output)
3336
{
3437
parent::__construct($input, $output);
@@ -45,6 +48,11 @@ private function isCiDetected(): bool
4548
return $this->isCiDetected;
4649
}
4750

51+
private function isAgentDetected(): bool
52+
{
53+
return $this->isAgentDetected ??= AgentDetector::detect()->isAgent;
54+
}
55+
4856
/**
4957
* @param string[] $headers
5058
* @param string[][] $rows
@@ -95,9 +103,10 @@ public function createProgressBar(int $max = 0): ProgressBar
95103
}
96104

97105
$ci = $this->isCiDetected();
98-
$this->progressBar->setOverwrite(!$ci);
106+
$agent = $this->isAgentDetected();
107+
$this->progressBar->setOverwrite(!$ci && !$agent);
99108

100-
if ($ci) {
109+
if ($ci || $agent) {
101110
$this->progressBar->minSecondsBetweenRedraws(15);
102111
$this->progressBar->maxSecondsBetweenRedraws(30);
103112
} elseif (DIRECTORY_SEPARATOR === '\\') {
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Command\ErrorFormatter;
4+
5+
use Override;
6+
use PHPStan\Testing\ErrorFormatterTestCase;
7+
use function putenv;
8+
9+
class AgentDetectedErrorFormatterTest extends ErrorFormatterTestCase
10+
{
11+
12+
#[Override]
13+
protected function setUp(): void
14+
{
15+
putenv('AI_AGENT');
16+
putenv('CURSOR_TRACE_ID');
17+
putenv('CURSOR_AGENT');
18+
putenv('GEMINI_CLI');
19+
putenv('CODEX_SANDBOX');
20+
putenv('AUGMENT_AGENT');
21+
putenv('OPENCODE_CLIENT');
22+
putenv('OPENCODE');
23+
putenv('CLAUDECODE');
24+
putenv('CLAUDE_CODE');
25+
putenv('REPL_ID');
26+
}
27+
28+
#[Override]
29+
protected function tearDown(): void
30+
{
31+
putenv('AI_AGENT');
32+
}
33+
34+
public function testIsAgentDetectedReturnsFalse(): void
35+
{
36+
$formatter = new AgentDetectedErrorFormatter(new JsonErrorFormatter(false));
37+
$this->assertFalse($formatter->isAgentDetected());
38+
}
39+
40+
public function testIsAgentDetectedReturnsTrueWithAiAgent(): void
41+
{
42+
putenv('AI_AGENT=test');
43+
$formatter = new AgentDetectedErrorFormatter(new JsonErrorFormatter(false));
44+
$this->assertTrue($formatter->isAgentDetected());
45+
}
46+
47+
public function testIsAgentDetectedReturnsTrueWithClaudeCode(): void
48+
{
49+
putenv('CLAUDE_CODE=1');
50+
$formatter = new AgentDetectedErrorFormatter(new JsonErrorFormatter(false));
51+
$this->assertTrue($formatter->isAgentDetected());
52+
}
53+
54+
public function testFormatErrorsProducesValidJson(): void
55+
{
56+
$formatter = new AgentDetectedErrorFormatter(new JsonErrorFormatter(false));
57+
58+
$exitCode = $formatter->formatErrors(
59+
$this->getAnalysisResult(1, 0),
60+
$this->getOutput(),
61+
);
62+
63+
$this->assertSame(1, $exitCode);
64+
$this->assertJsonStringEqualsJsonString(
65+
'{"totals":{"errors":0,"file_errors":1},"files":{"/data/folder/with space/and unicode 😃/project/folder with unicode 😃/file name with \\"spaces\\" and unicode 😃.php":{"errors":1,"messages":[{"message":"Foo","line":4,"ignorable":true}]}},"errors":[]}',
66+
$this->getOutputContent(),
67+
);
68+
}
69+
70+
public function testFormatErrorsNoErrors(): void
71+
{
72+
$formatter = new AgentDetectedErrorFormatter(new JsonErrorFormatter(false));
73+
74+
$exitCode = $formatter->formatErrors(
75+
$this->getAnalysisResult(0, 0),
76+
$this->getOutput(),
77+
);
78+
79+
$this->assertSame(0, $exitCode);
80+
$this->assertJsonStringEqualsJsonString(
81+
'{"totals":{"errors":0,"file_errors":0},"files":{},"errors":[]}',
82+
$this->getOutputContent(),
83+
);
84+
}
85+
86+
}

0 commit comments

Comments
 (0)