feat: lit/divi-design-snapshots und lit/divi-design-restore (Bereich presets)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Jan-Niclas Loosen
2026-10-16 17:52:20 +02:00
co-authored by Claude Sonnet 5.5
parent 2e8f723b01
commit 327f8d2d35
6 changed files with 322 additions and 0 deletions
@@ -0,0 +1,125 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\Design;
use LIT\MCP\Abilities\Ability;
use LIT\MCP\Abilities\Annotations;
use LIT\MCP\Abilities\Errors;
use WP_Error;
defined('ABSPATH') || exit;
/**
* lit/divi-design-restore (spec §5, G6): writes a snapshot back through the change pipeline – dry run with the diff
* to the current state, confirm with design_rev, and first a snapshot of the current state, so a restore can be
* undone as well. Etappe 4a restores presets; the other areas follow with their tools (4c, 4d).
*/
final class DiviDesignRestoreAbility implements Ability
{
private const ID = '/^(colors|variables|presets|theme_options)-\d{8}T\d{6}Z-[0-9a-f]{6}$/D';
public function __construct(
private DesignSystemWriter $writer,
private DesignChange $change,
private SnapshotStore $snapshots
) {
}
public function name(): string
{
return 'lit/divi-design-restore';
}
public function label(): string
{
return 'Designsystem wiederherstellen';
}
public function description(): string
{
return 'Stellt einen Snapshot des Designsystems wieder her (snapshot_id aus lit/divi-design-snapshots oder '
. 'einer Änderungsantwort). Zuerst Probelauf (Unterschied zum aktuellen Stand, design_rev), nach Rückfrage '
. 'beim Nutzer confirm: true; vorher wird der aktuelle Stand selbst gesichert.';
}
public function inputSchema(): array
{
return [
'type' => 'object',
'properties' => ['snapshot_id' => ['type' => 'string']] + DesignChange::inputProperties(),
'required' => ['snapshot_id'],
'additionalProperties' => false,
];
}
public function outputSchema(): ?array
{
return DesignChange::outputSchema();
}
public function annotations(): array
{
return Annotations::destructive(false);
}
public function isDirect(): bool
{
return false;
}
public function permission(array $input): bool|WP_Error
{
return DesignRights::presets($this->writer) ?? true;
}
public function execute(array $input): mixed
{
$refusal = DesignRights::presets($this->writer);
if ($refusal !== null) {
return $refusal;
}
$id = is_string($input['snapshot_id'] ?? null) ? $input['snapshot_id'] : '';
if (preg_match(self::ID, $id, $match) !== 1) {
return Errors::invalidInput(
'Ungültige Snapshot-ID.',
['snapshot_id' => 'z. B. presets-20261016T101500Z-a1b2c3']
);
}
if ($match[1] !== 'presets') {
return Errors::unsupported(
'Snapshots dieses Bereichs lassen sich noch nicht wiederherstellen.',
'Farben und Variablen folgen mit Etappe 4c, Theme-Optionen mit 4d.'
);
}
$snapshot = $this->snapshots->get('presets', $id);
if ($snapshot === null) {
return Errors::notFound(
sprintf('Snapshot %s gibt es nicht (mehr).', $id),
'Vorhandene Snapshots mit lit/divi-design-snapshots auflisten; je Bereich bleiben die letzten 20.'
);
}
return $this->change->run(
$this->name(),
new PresetArea($this->writer),
$input,
static function (array $raw) use ($snapshot, $id): DesignPlan {
$before = PresetBook::fromDivi($raw);
$after = PresetBook::fromDivi($snapshot['data']);
$diff = PresetDiff::between($before, $after);
return new DesignPlan(
$after->data(),
$diff,
sprintf('Snapshot %s wiederherstellen – %s', $id, PresetDiff::summary($diff)),
null,
['Seiten, die Presets verwenden, die es im Snapshot nicht gibt, behalten den Verweis; das Modul '
. 'fällt dann auf das Standard-Preset zurück.'],
true
);
}
);
}
}
@@ -0,0 +1,95 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Integrations\Divi\Design;
use LIT\MCP\Abilities\Ability;
use LIT\MCP\Abilities\Annotations;
use LIT\MCP\Abilities\Cursor;
use LIT\MCP\Abilities\Errors;
use WP_Error;
defined('ABSPATH') || exit;
/** lit/divi-design-snapshots (spec §5): the snapshots of one design area, newest first, without their content. */
final class DiviDesignSnapshotsAbility implements Ability
{
public function __construct(private SnapshotStore $snapshots)
{
}
public function name(): string
{
return 'lit/divi-design-snapshots';
}
public function label(): string
{
return 'Snapshots des Designsystems';
}
public function description(): string
{
return 'Listet die Snapshots eines Bereichs des Designsystems (area: presets, colors, variables, '
. 'theme_options; Standard presets), neueste zuerst, mit Zeitpunkt, Tool und Protokolleintrag. '
. 'Wiederherstellen mit lit/divi-design-restore.';
}
public function inputSchema(): array
{
return [
'type' => 'object',
'properties' => [
'area' => ['type' => 'string', 'enum' => SnapshotStore::AREAS],
] + Cursor::inputProperties(),
'additionalProperties' => false,
];
}
public function outputSchema(): ?array
{
return [
'type' => 'object',
'properties' => [
'area' => ['type' => 'string'],
'snapshots' => ['type' => 'array', 'items' => ['type' => 'object']],
'next_cursor' => ['type' => ['string', 'null']],
],
'required' => ['area', 'snapshots', 'next_cursor'],
];
}
public function annotations(): array
{
return Annotations::read();
}
public function isDirect(): bool
{
return false;
}
public function permission(array $input): bool|WP_Error
{
return current_user_can('edit_theme_options')
? true
: Errors::forbidden('Snapshots ansehen erfordert das Recht, das Design anzupassen (edit_theme_options).');
}
public function execute(array $input): mixed
{
$area = $input['area'] ?? 'presets';
if (! in_array($area, SnapshotStore::AREAS, true)) {
return Errors::invalidInput('Unbekannter Bereich.', ['area' => implode(', ', SnapshotStore::AREAS)]);
}
$offset = Cursor::decode(is_string($input['cursor'] ?? null) ? $input['cursor'] : null);
if ($offset === null) {
return Cursor::invalid();
}
$limit = Cursor::limit($input['limit'] ?? null);
$page = Cursor::page(array_slice($this->snapshots->list($area), $offset, $limit + 1), $limit, $offset);
return ['area' => $area, 'snapshots' => $page['items'], 'next_cursor' => $page['next_cursor']];
}
}
@@ -12,6 +12,8 @@ use LIT\MCP\Integrations\Divi\Design\DesignSystemWriter;
use LIT\MCP\Integrations\Divi\Design\DiviDesignSystemAbility;
use LIT\MCP\Integrations\Divi\Design\DesignChange;
use LIT\MCP\Integrations\Divi\Design\DiviPresetGetAbility;
use LIT\MCP\Integrations\Divi\Design\DiviDesignRestoreAbility;
use LIT\MCP\Integrations\Divi\Design\DiviDesignSnapshotsAbility;
use LIT\MCP\Integrations\Divi\Design\DiviPresetsDeleteAbility;
use LIT\MCP\Integrations\Divi\Design\DiviPresetsSetAbility;
use LIT\MCP\Integrations\Divi\Design\PresetComposer;
@@ -168,6 +170,8 @@ final class DiviIntegration implements Integration
$this->presetComposer(...)
),
new DiviPresetsDeleteAbility($this->designWriter, $this->designChange(), $this->globalUsages()),
new DiviDesignSnapshotsAbility(new SnapshotStore()),
new DiviDesignRestoreAbility($this->designWriter, $this->designChange(), new SnapshotStore()),
];
}