Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

IDEA - gemini plugin system thoughts #161

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion app/Actions/Shipments/CreateShipment.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@
use Lorisleiva\Actions\ActionRequest;
use Lorisleiva\Actions\Concerns\AsAction;
use Nette\NotImplementedException;
use App\Events\ShipmentCreated;
use Lorisleiva\Actions\Concerns\AsJob;

class CreateShipment
{
use AsAction;
use AsAction, AsJob;

public function handle(
array $customerIds,
Expand Down Expand Up @@ -58,6 +60,8 @@ public function handle(
]);
}

event(new ShipmentCreated($shipment));

DB::commit();

return $shipment;
Expand Down
75 changes: 75 additions & 0 deletions app/Console/Commands/ActivatePlugin.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php

namespace App\Console\Commands;

use App\Models\Plugin;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Artisan;

class ActivatePlugin extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'plugin:activate {plugin : The slug of the plugin}';

/**
* The console command description.
*
* @var string
*/
protected $description = 'Activate a plugin and run its migrations';

/**
* Execute the console command.
*/
public function handle()
{
/** @var \App\Models\Plugin $plugin */
$plugin = $this->argument('plugin');

$pluginSlug = $plugin->slug;

if ($plugin->is_active) {
$this->info("Plugin '{$pluginSlug}' is already active.");
return 0;
}

$pluginPath = base_path("plugins/{$pluginSlug}");
$manifestPath = $pluginPath . '/plugin.json';

if (!file_exists($manifestPath)) {
$this->error("Plugin manifest not found for '{$pluginSlug}'.");
return 1;
}

$manifest = json_decode(file_get_contents($manifestPath), true);

if (isset($manifest['migrations'])) {
$migrationsPath = $pluginPath . '/' . $manifest['migrations'];
if (is_dir($migrationsPath)) {
$this->info("Running migrations for plugin '{$pluginSlug}'...");
// We need to specify the path to plugin's migrations
Artisan::call('migrate', [
'--path' => $migrationsPath,
'--force' => true, // Add --force to run in production if needed
]);
$this->info(Artisan::output()); // Display migration output
} else {
$this->info("No migrations found for plugin '{$pluginSlug}'.");
}
}

$plugin->is_active = true;
$plugin->save();
$this->info("Plugin '{$pluginSlug}' activated successfully.");

// Clear config cache in case plugin configuration is involved
Artisan::call('config:clear');
$this->info("Configuration cache cleared.");

return 0;
}
}
54 changes: 54 additions & 0 deletions app/Console/Commands/DeactivatePlugin.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

namespace App\Console\Commands;

use App\Models\Plugin;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Artisan;

class DeactivatePlugin extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'plugin:deactivate {slug}';

/**
* The console command description.
*
* @var string
*/
protected $description = 'Deactivate a plugin';

/**
* Execute the console command.
*/
public function handle()
{
$pluginSlug = $this->argument('slug');
$plugin = Plugin::where('slug', $pluginSlug)->first();

if (!$plugin) {
$this->error("Plugin with slug '{$pluginSlug}' not found.");
return 1;
}

if (!$plugin->is_active) {
$this->info("Plugin '{$pluginSlug}' is already inactive.");
return 0;
}

$plugin->is_active = false;
$plugin->save();
$this->info("Plugin '{$pluginSlug}' deactivated successfully.");

// Clear config cache in case plugin configuration is involved
Artisan::call('config:clear');
$this->info("Configuration cache cleared.");


return 0;
}
}
36 changes: 36 additions & 0 deletions app/Events/ShipmentCreated.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

namespace App\Events;

use App\Models\Shipments\Shipment;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class ShipmentCreated
{
use Dispatchable, InteractsWithSockets, SerializesModels;

public Shipment $shipment;

/**
* Create a new event instance.
*/
public function __construct(Shipment $shipment)
{
$this->shipment = $shipment;
}

/**
* Get the channels the event should broadcast on.
*
* @return array<int, \Illuminate\Broadcasting\Channel>
*/
public function broadcastOn(): array
{
return [
new PrivateChannel('channel-name'),
];
}
}
136 changes: 136 additions & 0 deletions app/Http/Controllers/Admin/PluginController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
<?php

namespace App\Http\Controllers\Admin;

use App\Http\Controllers\Controller;
use App\Models\Plugin;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Response;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\File;
use ZipArchive;

class PluginController extends Controller
{
public function index(): Response
{
$plugins = Plugin::all()->map(function ($plugin) {
$manifestPath = base_path("plugins/{$plugin->slug}/plugin.json");
$manifest = file_exists($manifestPath) ? json_decode(file_get_contents($manifestPath), true) : [];
return [
'id' => $plugin->id,
'slug' => $plugin->slug,
'is_active' => $plugin->is_active,
'name' => $manifest['name'] ?? $plugin->slug, // Default to slug if name not in manifest
'description' => $manifest['description'] ?? 'No description',
'version' => $manifest['version'] ?? 'N/A',
'author' => $manifest['author'] ?? 'Unknown',
];
});

return Inertia::render('Admin/Plugins/Index', [
'plugins' => $plugins,
]);
}

public function activate(string $slug)
{
/** @var \App\Models\Plugin $plugin */
$plugin = Plugin::where('slug', $slug)->firstOrFail();

Artisan::call('plugin:activate', ['plugin' => $plugin]);
return back()->with('success', "Plugin '{$plugin->name}' activated.");
}

public function deactivate(string $slug)
{
/** @var \App\Models\Plugin $plugin */
$plugin = Plugin::where('slug', $slug)->firstOrFail();

Artisan::call('plugin:deactivate', ['slug' => $plugin->slug]);
return back()->with('success', "Plugin '{$plugin->name}' deactivated.");
}

public function install(Request $request)
{
$request->validate([
'plugin_zip' => 'required|file|mimes:zip',
]);

$file = $request->file('plugin_zip');
$filename = $file->getClientOriginalName();
$tempPath = storage_path('app/temp_plugins'); // Temporary directory to store uploaded zip
$pluginSlug = ''; // Will be extracted from manifest

if (!File::isDirectory($tempPath)) {
File::makeDirectory($tempPath, 0755, true); // Create temp directory if not exists
}

$file->move($tempPath, $filename);
$zipFilePath = "{$tempPath}/{$filename}";
$extractPath = base_path('plugins');


$zip = new ZipArchive;
if ($zip->open($zipFilePath) === TRUE) {
// Extract to a temporary directory first to validate manifest before placing in plugins dir
$tempExtractPath = "{$tempPath}/" . pathinfo($filename, PATHINFO_FILENAME);
if (!File::isDirectory($tempExtractPath)) {
File::makeDirectory($tempExtractPath, 0755, true);
}
$zip->extractTo($tempExtractPath);
$zip->close();

// Validate manifest
$manifestPath = "{$tempExtractPath}/plugin.json";
if (!file_exists($manifestPath)) {
File::deleteDirectory($tempExtractPath); // Clean up temp directory
File::delete($zipFilePath);
return back()->withErrors(['plugin_zip' => 'Plugin manifest (plugin.json) not found in the zip file.']);
}

$manifest = json_decode(file_get_contents($manifestPath), true);
if (!$manifest || !isset($manifest['slug'])) {
File::deleteDirectory($tempExtractPath);
File::delete($zipFilePath);
return back()->withErrors(['plugin_zip' => 'Invalid plugin manifest (plugin.json). Slug is missing or invalid JSON.']);
}
$pluginSlug = $manifest['slug'];

// Check if plugin with same slug already exists
if (Plugin::where('slug', $pluginSlug)->exists()) {
File::deleteDirectory($tempExtractPath);
File::delete($zipFilePath);
return back()->withErrors(['plugin_zip' => "Plugin with slug '{$pluginSlug}' already exists."]);
}


// Move plugin directory to plugins path
$pluginTargetPath = "{$extractPath}/{$pluginSlug}";
if (File::isDirectory($pluginTargetPath)) {
File::deleteDirectory($pluginTargetPath); // Ensure no existing directory
}


File::moveDirectory($tempExtractPath, $pluginTargetPath);


// Create plugin database record (initially inactive)
Plugin::create(['slug' => $pluginSlug, 'is_active' => false]);


// Clean up temp files
File::deleteDirectory($tempExtractPath);
File::delete($zipFilePath);


return back()->with('success', "Plugin '".($manifest['name'] ?? $pluginSlug)."' installed successfully. It is inactive by default. Activate it from the plugin list.");

} else {
File::delete($zipFilePath);
return back()->withErrors(['plugin_zip' => 'Could not open zip file.']);
}
}
}
12 changes: 12 additions & 0 deletions app/Http/Controllers/Shipments/ShipmentController.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
use App\Models\Shipments\TrailerType;
use App\Models\Customers\Customer;
use Inertia\Inertia;
use App\Events\ShipmentCreated;
use Illuminate\Http\Request;

class ShipmentController extends ResourceSearchController
{
Expand Down Expand Up @@ -83,4 +85,14 @@ public function destroy(Shipment $shipment)
{
//
}

public function store(Request $request)
{
$shipment = Shipment::create($request->all());

event(new ShipmentCreated($shipment)); // Dispatch the event

// ... rest of your logic, like returning a response ...
return response()->json(['shipment' => $shipment], 201);
}
}
3 changes: 3 additions & 0 deletions app/Http/Middleware/HandleInertiaRequests.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
use App\Enums\Permission;
use Illuminate\Http\Request;
use Inertia\Middleware;
use Illuminate\Support\Facades\App;
use App\Services\FrontendPluginRegistry;

class HandleInertiaRequests extends Middleware
{
Expand Down Expand Up @@ -39,6 +41,7 @@ public function share(Request $request): array
'app' => [
'name' => config('app.name'),
],
'pluginSidebarMenuItems' => App::make(FrontendPluginRegistry::class)->getSidebarMenuItems(),
];
}
}
16 changes: 16 additions & 0 deletions app/Models/Plugin.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Plugin extends Model
{
use HasFactory;

protected $fillable = [
'slug',
'is_active',
];
}
Loading
Loading