Skip to content

Commit 2d6b103

Browse files
committed
Allow support for non-default vendor-dir by using official Composer API
1 parent 4130efb commit 2d6b103

File tree

4 files changed

+193
-60
lines changed

4 files changed

+193
-60
lines changed

src/mate/bin/mate.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,12 @@
2323
foreach ($autoloadPaths as $autoloadPath) {
2424
if (file_exists($autoloadPath)) {
2525
require_once $autoloadPath;
26-
$root = dirname(realpath($autoloadPath), 2);
26+
if (method_exists(Composer\InstalledVersions::class, 'getInstallPath')) {
27+
$root = Composer\InstalledVersions::getInstallPath('__root__');
28+
} else {
29+
$root = dirname(realpath($autoloadPath), 2);
30+
}
31+
2732
break;
2833
}
2934
}

src/mate/composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
},
4141
"require-dev": {
4242
"ext-simplexml": "*",
43+
"composer/composer": "^2",
4344
"helgesverre/toon": "^3.1",
4445
"phpstan/phpstan": "^2.1",
4546
"phpstan/phpstan-phpunit": "^2.0",

src/mate/src/Agent/AgentInstructionsAggregator.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111

1212
namespace Symfony\AI\Mate\Agent;
1313

14+
use Composer\Composer;
15+
use Composer\Factory;
16+
use Composer\IO\NullIO;
1417
use Psr\Log\LoggerInterface;
1518
use Symfony\AI\Mate\Discovery\ComposerExtensionDiscovery;
1619

@@ -30,6 +33,8 @@
3033
*/
3134
final class AgentInstructionsAggregator
3235
{
36+
private ?Composer $composer = null;
37+
3338
/**
3439
* @param array<string, ExtensionData> $extensions
3540
*/
@@ -87,6 +92,16 @@ private function loadExtensionInstructions(string $packageName, array $data): ?s
8792
}
8893

8994
$fullPath = $this->rootDir.'/vendor/'.$packageName.'/'.ltrim($instructionsPath, '/');
95+
$composer = $this->getComposer();
96+
97+
if ($composer instanceof Composer) {
98+
$package = $composer->getRepositoryManager()->getLocalRepository()->findPackage($packageName, '*');
99+
100+
if ($package !== null) { // weird if it is not
101+
$packagePath = $composer->getInstallationManager()->getInstallPath($package);
102+
$fullPath = $packagePath . DIRECTORY_SEPARATOR . ltrim($instructionsPath, '/');
103+
}
104+
}
90105

91106
return $this->readInstructionsFile($fullPath, $packageName);
92107
}
@@ -178,4 +193,20 @@ private function deepenMarkdownHeadings(string $content): string
178193

179194
return implode("\n", $lines);
180195
}
196+
197+
private function getComposer(): ?Composer
198+
{
199+
if ($this->composer !== null) {
200+
return $this->composer;
201+
}
202+
203+
if (class_exists(Factory::class)) {
204+
$result = Factory::create(new NullIO(), disableScripts: true);
205+
$this->composer = $result;
206+
207+
return $result;
208+
}
209+
210+
return null;
211+
}
181212
}

src/mate/src/Discovery/ComposerExtensionDiscovery.php

Lines changed: 155 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
1111

1212
namespace Symfony\AI\Mate\Discovery;
1313

14+
use Composer\Composer;
15+
use Composer\Factory;
16+
use Composer\IO\NullIO;
17+
use Composer\Util\Filesystem;
1418
use Psr\Log\LoggerInterface;
1519

1620
/**
@@ -46,6 +50,8 @@ final class ComposerExtensionDiscovery
4650
*/
4751
private ?array $installedPackages = null;
4852

53+
private ?Composer $composer = null;
54+
4955
public function __construct(
5056
private string $rootDir,
5157
private LoggerInterface $logger,
@@ -161,63 +167,19 @@ private function getInstalledPackages(): array
161167
return $this->installedPackages;
162168
}
163169

164-
$installedJsonPath = $this->rootDir.'/vendor/composer/installed.json';
165-
if (!file_exists($installedJsonPath)) {
166-
$this->logger->warning('Composer installed.json not found', ['path' => $installedJsonPath]);
167-
168-
return $this->installedPackages = [];
169-
}
170-
171-
$content = file_get_contents($installedJsonPath);
172-
if (false === $content) {
173-
$this->logger->warning('Could not read installed.json', [
174-
'path' => $installedJsonPath,
175-
'error' => error_get_last()['message'] ?? 'Unknown error',
176-
]);
177-
178-
return $this->installedPackages = [];
179-
}
180-
181-
try {
182-
$data = json_decode($content, true, 512, \JSON_THROW_ON_ERROR);
183-
} catch (\JsonException $e) {
184-
$this->logger->error('Invalid JSON in installed.json', ['error' => $e->getMessage()]);
185-
186-
return $this->installedPackages = [];
187-
}
188-
189-
if (!\is_array($data)) {
190-
return $this->installedPackages = [];
191-
}
192-
193-
// Handle both formats: {"packages": [...]} and direct array
194-
$packages = $data['packages'] ?? $data;
195-
if (!\is_array($packages)) {
196-
return $this->installedPackages = [];
197-
}
198-
199-
$indexed = [];
200-
foreach ($packages as $package) {
201-
if (!\is_array($package) || !isset($package['name']) || !\is_string($package['name'])) {
202-
continue;
203-
}
170+
$composer = $this->getComposer();
204171

205-
/** @var array{
206-
* name: string,
207-
* extra: array<string, mixed>,
208-
* } $validPackage */
209-
$validPackage = [
210-
'name' => $package['name'],
211-
'extra' => [],
212-
];
172+
if ($composer !== null) {
173+
$indexed = [];
213174

214-
if (isset($package['extra']) && \is_array($package['extra'])) {
215-
/** @var array<string, mixed> $extra */
216-
$extra = $package['extra'];
217-
$validPackage['extra'] = $extra;
175+
foreach ($composer->getRepositoryManager()->getLocalRepository()->getPackages() as $package) {
176+
$indexed[$package->getName()] = [
177+
'name' => $package->getName(),
178+
'extra' => $package->getExtra(),
179+
];
218180
}
219-
220-
$indexed[$package['name']] = $validPackage;
181+
} else {
182+
$indexed = $this->getPackagesWithoutComposer();
221183
}
222184

223185
return $this->installedPackages = $indexed;
@@ -234,10 +196,25 @@ private function getInstalledPackages(): array
234196
private function extractScanDirs(array $package, string $packageName): array
235197
{
236198
$aiMateConfig = $package['extra']['ai-mate'] ?? null;
199+
$composer = $this->getComposer();
200+
$package = null;
201+
202+
if ($composer instanceof Composer) {
203+
$package = $composer->getRepositoryManager()->getLocalRepository()->findPackage($packageName, '*');
204+
}
205+
237206
if (null === $aiMateConfig) {
238207
// Default: scan package root directory if no config provided
239208
$defaultDir = 'vendor/'.$packageName;
240-
if (is_dir($this->rootDir.'/'.$defaultDir)) {
209+
$fullPath = $this->rootDir.'/'.$defaultDir;
210+
211+
if ($package !== null) {
212+
$packagePath = $composer->getInstallationManager()->getInstallPath($package);
213+
$defaultDir = (new Filesystem())->findShortestPath($this->rootDir, $packagePath);
214+
$fullPath = $packagePath;
215+
}
216+
217+
if (is_dir($fullPath)) {
241218
return [$defaultDir];
242219
}
243220

@@ -268,16 +245,25 @@ private function extractScanDirs(array $package, string $packageName): array
268245
continue;
269246
}
270247

271-
$fullPath = 'vendor/'.$packageName.'/'.ltrim($dir, '/');
272-
if (!is_dir($this->rootDir.'/'.$fullPath)) {
248+
$packageScanDir = 'vendor/'.$packageName.'/'.ltrim($dir, '/');
249+
$fullPath = $this->rootDir.'/'.$packageScanDir;
250+
251+
if ($package !== null) {
252+
$packagePath = $composer->getInstallationManager()->getInstallPath($package);
253+
$packageScanDir = $packagePath.'/'.ltrim($dir, '/');
254+
$packageScanDir = (new Filesystem())->findShortestPath($this->rootDir, $packageScanDir);
255+
$fullPath = $packageScanDir;
256+
}
257+
258+
if (!is_dir($fullPath)) {
273259
$this->logger->warning('Scan directory does not exist', [
274260
'package' => $packageName,
275-
'directory' => $fullPath,
261+
'directory' => $packageScanDir,
276262
]);
277263
continue;
278264
}
279265

280-
$validDirs[] = $fullPath;
266+
$validDirs[] = $packageScanDir;
281267
}
282268

283269
return $validDirs;
@@ -316,13 +302,26 @@ private function extractIncludeFiles(array $package, string $packageName): array
316302
return [];
317303
}
318304

305+
$composer = $this->getComposer();
306+
$package = null;
307+
308+
if ($composer instanceof Composer) {
309+
$package = $composer->getRepositoryManager()->getLocalRepository()->findPackage($packageName, '*');
310+
}
311+
319312
$validFiles = [];
320313
foreach ($includes as $file) {
321314
if (!\is_string($file) || '' === trim($file) || str_contains($file, '..')) {
322315
continue;
323316
}
324317

325318
$fullPath = $this->rootDir.'/vendor/'.$packageName.'/'.ltrim($file, '/');
319+
320+
if ($package !== null) {
321+
$packagePath = $composer->getInstallationManager()->getInstallPath($package);
322+
$fullPath = $packagePath.'/'.ltrim($file, '/');
323+
}
324+
326325
if (!file_exists($fullPath)) {
327326
$this->logger->warning('Include file does not exist', [
328327
'package' => $packageName,
@@ -371,8 +370,21 @@ private function extractInstructions(array $package, string $packageName): ?stri
371370
return null;
372371
}
373372

373+
$composer = $this->getComposer();
374+
$package = null;
375+
376+
if ($composer instanceof Composer) {
377+
$package = $composer->getRepositoryManager()->getLocalRepository()->findPackage($packageName, '*');
378+
}
379+
374380
// Validate file exists
375381
$fullPath = $this->rootDir.'/vendor/'.$packageName.'/'.ltrim($agentInstructions, '/');
382+
383+
if ($package !== null) {
384+
$packagePath = $composer->getInstallationManager()->getInstallPath($package);
385+
$fullPath = $packagePath.'/'.ltrim($agentInstructions, '/');
386+
}
387+
376388
if (!file_exists($fullPath)) {
377389
$this->logger->warning('Agent instructions file does not exist', [
378390
'package' => $packageName,
@@ -420,4 +432,88 @@ private function extractAiMateConfigString(array $composer, string $key): ?strin
420432

421433
return $value;
422434
}
435+
436+
private function getPackagesWithoutComposer(): array
437+
{
438+
$installedJsonPath = $this->rootDir.'/vendor/composer/installed.json';
439+
440+
if (!file_exists($installedJsonPath)) {
441+
$this->logger->warning('Composer installed.json not found', ['path' => $installedJsonPath]);
442+
443+
return [];
444+
}
445+
446+
$content = file_get_contents($installedJsonPath);
447+
if (false === $content) {
448+
$this->logger->warning('Could not read installed.json', [
449+
'path' => $installedJsonPath,
450+
'error' => error_get_last()['message'] ?? 'Unknown error',
451+
]);
452+
453+
return [];
454+
}
455+
456+
try {
457+
$data = json_decode($content, true, 512, \JSON_THROW_ON_ERROR);
458+
} catch (\JsonException $e) {
459+
$this->logger->error('Invalid JSON in installed.json', ['error' => $e->getMessage()]);
460+
461+
return [];
462+
}
463+
464+
if (!\is_array($data)) {
465+
return [];
466+
}
467+
468+
// Handle both formats: {"packages": [...]} and direct array
469+
$packages = $data['packages'] ?? $data;
470+
471+
if (!\is_array($packages)) {
472+
return [];
473+
}
474+
475+
$indexed = [];
476+
477+
foreach ($packages as $package) {
478+
if (!\is_array($package) || !isset($package['name']) || !\is_string($package['name'])) {
479+
continue;
480+
}
481+
482+
/** @var array{
483+
* name: string,
484+
* extra: array<string, mixed>,
485+
* } $validPackage
486+
*/
487+
$validPackage = [
488+
'name' => $package['name'],
489+
'extra' => [],
490+
];
491+
492+
if (isset($package['extra']) && \is_array($package['extra'])) {
493+
/** @var array<string, mixed> $extra */
494+
$extra = $package['extra'];
495+
$validPackage['extra'] = $extra;
496+
}
497+
498+
$indexed[$package['name']] = $validPackage;
499+
}
500+
501+
return $indexed;
502+
}
503+
504+
private function getComposer(): ?Composer
505+
{
506+
if ($this->composer !== null) {
507+
return $this->composer;
508+
}
509+
510+
if (class_exists(Factory::class)) {
511+
$result = Factory::create(new NullIO(), disableScripts: true);
512+
$this->composer = $result;
513+
514+
return $result;
515+
}
516+
517+
return null;
518+
}
423519
}

0 commit comments

Comments
 (0)