Skip to content

Commit 07b4162

Browse files
committed
feat: initial v0.0.1 of Boost
1 parent 6ba9a11 commit 07b4162

34 files changed

+1473
-472
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
.idea
2+
.DS_Store
23
/node_modules
34
/public/app.js.LICENSE.txt
45
/vendor

composer.json

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,12 @@
2020
],
2121
"repositories": [
2222
{
23-
"type": "path",
24-
"url": "../mcp"
23+
"type": "git",
24+
"url": "[email protected]:laravel/mcp.git"
25+
},
26+
{
27+
"type": "git",
28+
"url": "[email protected]:laravel/roster.git"
2529
}
2630
],
2731
"require": {
@@ -30,7 +34,9 @@
3034
"illuminate/contracts": "^11.0|^12.0",
3135
"illuminate/routing": "^11.0|^12.0",
3236
"illuminate/support": "^11.0|^12.0",
33-
"laravel/mcp": "dev-main"
37+
"laravel/mcp": "dev-main",
38+
"laravel/prompts": "^0.3",
39+
"laravel/roster": "dev-main"
3440
},
3541
"require-dev": {
3642
"laravel/pint": "^1.23",

config/boost.php

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,25 @@
11
<?php
22

33
return [
4-
4+
'hosted' => [
5+
'token' => env('BOOST_HOSTED_TOKEN'),
6+
],
7+
'mcp' => [
8+
'tools' => [
9+
'exclude' => [ // Exclude built-in tools
10+
\Laravel\Boost\Mcp\Tools\LastError::class,
11+
],
12+
'include' => [ // Include your own tools
13+
\Laravel\Boost\Mcp\Tools\LastError::class,
14+
],
15+
],
16+
'resources' => [
17+
'exclude' => [],
18+
'include' => [],
19+
],
20+
'prompts' => [
21+
'exclude' => [],
22+
'include' => [],
23+
],
24+
],
525
];

phpstan.neon.dist

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,4 @@ parameters:
33
- config
44
- src
55

6-
level: 0
7-
8-
ignoreErrors:
9-
- "#Unsafe usage of new static\\(\\)#"
6+
level: 8

pint.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"preset": "laravel",
3+
"exclude": [
4+
"stubs"
5+
]
6+
}

src/Boost.php

Lines changed: 0 additions & 40 deletions
This file was deleted.

src/BoostServiceProvider.php

Lines changed: 34 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -5,69 +5,65 @@
55
use Illuminate\Support\ServiceProvider;
66
use Laravel\Boost\Mcp\Boost;
77
use Laravel\Mcp\Server\Facades\Mcp;
8+
use Laravel\Roster\Roster;
89

910
class BoostServiceProvider extends ServiceProvider
1011
{
11-
/**
12-
* Register any package services.
13-
*
14-
* @return void
15-
*/
16-
public function register()
12+
public function register(): void
1713
{
1814
$this->mergeConfigFrom(
19-
__DIR__.'/../config/boost.php', 'boost'
15+
__DIR__ . '/../config/boost.php', 'boost'
2016
);
17+
18+
$this->app->singleton(Roster::class, function () {
19+
$composerLockPath = base_path('composer.lock');
20+
$packageLockPath = base_path('package-lock.json');
21+
22+
$cacheKey = 'boost.roster.scan';
23+
$lastModified = max(
24+
file_exists($composerLockPath) ? filemtime($composerLockPath) : 0,
25+
file_exists($packageLockPath) ? filemtime($packageLockPath) : 0
26+
);
27+
28+
$cached = cache()->get($cacheKey);
29+
if ($cached && isset($cached['timestamp']) && $cached['timestamp'] >= $lastModified) {
30+
return $cached['roster'];
31+
}
32+
33+
$roster = Roster::scan(base_path());
34+
cache()->put($cacheKey, [
35+
'roster' => $roster,
36+
'timestamp' => time(),
37+
], now()->addHours(24));
38+
39+
return $roster;
40+
});
2141
}
2242

23-
/**
24-
* Bootstrap any package services.
25-
*
26-
* @return void
27-
*/
28-
public function boot()
43+
public function boot(): void
2944
{
45+
/* @phpstan-ignore-next-line */
3046
Mcp::local('laravel-boost', Boost::class);
3147

32-
$this->registerResources();
3348
$this->registerPublishing();
3449
$this->registerCommands();
3550
}
3651

37-
/**
38-
* Register the package resources.
39-
*
40-
* @return void
41-
*/
42-
protected function registerResources()
43-
{
44-
$this->loadViewsFrom(__DIR__.'/../resources/views', 'boost');
45-
}
46-
47-
/**
48-
* Register the package's publishable resources.
49-
*
50-
* @return void
51-
*/
52-
protected function registerPublishing()
52+
protected function registerPublishing(): void
5353
{
5454
if ($this->app->runningInConsole()) {
5555
$this->publishes([
56-
__DIR__.'/../config/boost.php' => config_path('boost.php'),
56+
__DIR__ . '/../config/boost.php' => config_path('boost.php'),
5757
], 'boost-config');
5858
}
5959
}
6060

61-
/**
62-
* Register the package's commands.
63-
*
64-
* @return void
65-
*/
66-
protected function registerCommands()
61+
protected function registerCommands(): void
6762
{
6863
if ($this->app->runningInConsole()) {
6964
$this->commands([
70-
Console\InstallCommand::class,
65+
Console\StartCommand::class,
66+
Console\InstallCommand::class
7167
]);
7268
}
7369
}

src/Concerns/MakesHttpRequests.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Laravel\Boost\Concerns;
6+
7+
use Illuminate\Http\Client\PendingRequest;
8+
use Illuminate\Http\Client\Response;
9+
use Illuminate\Support\Facades\Http;
10+
11+
trait MakesHttpRequests
12+
{
13+
public function client(): PendingRequest
14+
{
15+
return Http::withHeaders([
16+
'User-Agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:140.0) Gecko/20100101 Firefox/140.0 Laravel Boost',
17+
]);
18+
}
19+
20+
public function get(string $url): Response
21+
{
22+
return $this->client()->get($url);
23+
}
24+
}

src/Concerns/ReadsLogs.php

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
<?php
2+
3+
namespace Laravel\Boost\Concerns;
4+
5+
use Illuminate\Support\Facades\Config;
6+
7+
trait ReadsLogs
8+
{
9+
/**
10+
* Regular expression fragments and default chunk-window sizes used when
11+
* scanning log files. Declaring them once keeps every consumer in sync.
12+
*/
13+
private const TIMESTAMP_REGEX = '\\[\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}\\]';
14+
15+
private const ENTRY_SPLIT_REGEX = '/(?='.self::TIMESTAMP_REGEX.')/';
16+
17+
private const ERROR_ENTRY_REGEX = '/^'.self::TIMESTAMP_REGEX.'.*\\.ERROR:/';
18+
19+
private const CHUNK_SIZE_START = 64 * 1024; // 64 kB
20+
21+
private const CHUNK_SIZE_MAX = 1 * 1024 * 1024; // 1 MB
22+
23+
/**
24+
* Resolve the current log file path based on Laravel's logging configuration.
25+
*/
26+
protected function resolveLogFilePath(): string
27+
{
28+
$channel = Config::get('logging.default');
29+
$channelConfig = Config::get("logging.channels.{$channel}");
30+
31+
if (($channelConfig['driver'] ?? null) === 'daily') {
32+
return storage_path('logs/laravel-'.date('Y-m-d').'.log');
33+
}
34+
35+
return storage_path('logs/laravel.log');
36+
}
37+
38+
/**
39+
* Determine if the given line (or entry) is an ERROR log entry.
40+
*/
41+
protected function isErrorEntry(string $line): bool
42+
{
43+
return preg_match(self::ERROR_ENTRY_REGEX, $line) === 1;
44+
}
45+
46+
/**
47+
* Retrieve the last $count complete PSR-3 log entries from the log file using
48+
* chunked reading instead of character-by-character reverse scanning.
49+
*
50+
* @return string[]
51+
*/
52+
protected function readLastLogEntries(string $logFile, int $count): array
53+
{
54+
$chunkSize = self::CHUNK_SIZE_START;
55+
56+
do {
57+
$entries = $this->scanLogChunkForEntries($logFile, $chunkSize);
58+
59+
if (count($entries) >= $count || $chunkSize >= self::CHUNK_SIZE_MAX) {
60+
break;
61+
}
62+
63+
$chunkSize *= 2;
64+
} while (true);
65+
66+
return array_slice($entries, -$count);
67+
}
68+
69+
/**
70+
* Return the most recent ERROR log entry, or null if none exists within the
71+
* inspected window.
72+
*/
73+
protected function readLastErrorEntry(string $logFile): ?string
74+
{
75+
$chunkSize = self::CHUNK_SIZE_START;
76+
77+
do {
78+
$entries = $this->scanLogChunkForEntries($logFile, $chunkSize);
79+
80+
for ($i = count($entries) - 1; $i >= 0; $i--) {
81+
if ($this->isErrorEntry($entries[$i])) {
82+
return trim($entries[$i]);
83+
}
84+
}
85+
86+
if ($chunkSize >= self::CHUNK_SIZE_MAX) {
87+
return null;
88+
}
89+
90+
$chunkSize *= 2;
91+
} while (true);
92+
}
93+
94+
/**
95+
* Scan the last $chunkSize bytes of the log file and return an array of
96+
* complete log entries (oldest ➜ newest).
97+
*
98+
* @return string[]
99+
*/
100+
private function scanLogChunkForEntries(string $logFile, int $chunkSize): array
101+
{
102+
$fileSize = filesize($logFile);
103+
if ($fileSize === false) {
104+
return [];
105+
}
106+
107+
$handle = fopen($logFile, 'r');
108+
if (! $handle) {
109+
return [];
110+
}
111+
112+
try {
113+
$offset = max($fileSize - $chunkSize, 0);
114+
fseek($handle, $offset);
115+
116+
// If we started mid-line, discard the partial line to align to next newline.
117+
if ($offset > 0) {
118+
fgets($handle);
119+
}
120+
121+
$content = stream_get_contents($handle);
122+
if ($content === false) {
123+
return [];
124+
}
125+
126+
// Split by beginning-of-entry look-ahead (PSR-3 timestamp pattern).
127+
$entries = preg_split(self::ENTRY_SPLIT_REGEX, $content, -1, PREG_SPLIT_NO_EMPTY);
128+
if (! $entries) {
129+
return [];
130+
}
131+
132+
return $entries; // already in chronological order relative to chunk
133+
} finally {
134+
fclose($handle);
135+
}
136+
}
137+
}

0 commit comments

Comments
 (0)