| Server IP : 3.147.158.171 / Your IP : 216.73.216.216 Web Server : Apache/2.4.67 (Amazon Linux) OpenSSL/3.5.5 System : Linux ip-172-31-2-178.us-east-2.compute.internal 6.1.172-216.329.amzn2023.x86_64 #1 SMP PREEMPT_DYNAMIC Wed May 20 06:31:34 UTC 2026 x86_64 User : ec2-user ( 1000) PHP Version : 8.4.21 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : OFF Directory : /tsai/repo/.claude/skills/store-ia/ |
Upload File : |
<?php
/**
* Scike store IA builder — run with: drush scr store_ia.php -- <mode>
*
* build Create (or regenerate) every page, menu, and the Scike product from
* manifest.json. Idempotent: it first removes anything it previously
* created, then rebuilds, so re-running after editing the IA is safe.
* clean Remove everything this skill created and restore the prior front page.
* status Report what is currently tracked.
*
* Everything created is recorded in Drupal state under the `tsai_store_ia.*`
* keys, so cleanup is reliable even across sessions.
*/
use Drupal\commerce_price\Price;
$MARKER = 'tsai_store_ia';
$state = \Drupal::state();
$etm = \Drupal::entityTypeManager();
// --- Resolve mode (works with or without the `--` separator). ---------------
$argv = $_SERVER['argv'] ?? [];
$mode = 'status';
foreach (['build', 'clean', 'status'] as $m) {
if (in_array($m, $argv, TRUE)) {
$mode = $m;
}
}
// --- Load the manifest next to this script. ---------------------------------
$dir = __DIR__;
$manifest = json_decode(file_get_contents("$dir/manifest.json"), TRUE);
if (!$manifest) {
echo "ERROR: could not read manifest.json\n";
return;
}
$format = $manifest['text_format'] ?? 'full_html';
function body_html(string $dir, array $entry): string {
if (empty($entry['body_file'])) {
return '';
}
$path = $dir . '/' . $entry['body_file'];
return is_file($path) ? file_get_contents($path) : '';
}
// ---------------------------------------------------------------------------
// Delete everything currently tracked in state (pages, menu links, product,
// product variations, attribute values). Does NOT touch the front-page record.
// ---------------------------------------------------------------------------
function delete_tracked(string $MARKER, $state, $etm): void {
// Menu links.
$ids = $state->get("$MARKER.menu_links", []);
if ($ids) {
$links = $etm->getStorage('menu_link_content')->loadMultiple($ids);
if ($links) {
$etm->getStorage('menu_link_content')->delete($links);
}
echo " removed " . count($ids) . " menu link(s)\n";
}
$state->delete("$MARKER.menu_links");
// Product (variations cascade-delete with it, but delete explicitly too).
$pid = $state->get("$MARKER.product");
if ($pid && ($product = $etm->getStorage('commerce_product')->load($pid))) {
$product->delete();
echo " removed product $pid\n";
}
$state->delete("$MARKER.product");
$varids = $state->get("$MARKER.variations", []);
if ($varids) {
$vars = $etm->getStorage('commerce_product_variation')->loadMultiple($varids);
if ($vars) {
$etm->getStorage('commerce_product_variation')->delete($vars);
}
}
$state->delete("$MARKER.variations");
// Attribute values we created.
$avids = $state->get("$MARKER.attr_values", []);
if ($avids) {
$avs = $etm->getStorage('commerce_product_attribute_value')->loadMultiple($avids);
if ($avs) {
$etm->getStorage('commerce_product_attribute_value')->delete($avs);
}
echo " removed " . count($avids) . " plan attribute value(s)\n";
}
$state->delete("$MARKER.attr_values");
// Placed blocks (e.g. the footer menu block).
$bids = $state->get("$MARKER.blocks", []);
if ($bids) {
$blocks = $etm->getStorage('block')->loadMultiple($bids);
if ($blocks) {
$etm->getStorage('block')->delete($blocks);
}
echo " removed " . count($bids) . " placed block(s)\n";
}
$state->delete("$MARKER.blocks");
// Pages.
$pages = $state->get("$MARKER.pages", []);
if ($pages) {
$nodes = $etm->getStorage('node')->loadMultiple(array_values($pages));
if ($nodes) {
$etm->getStorage('node')->delete($nodes);
}
echo " removed " . count($pages) . " page(s)\n";
}
$state->delete("$MARKER.pages");
}
// ---------------------------------------------------------------------------
if ($mode === 'status') {
$pages = $state->get("$MARKER.pages", []);
$links = $state->get("$MARKER.menu_links", []);
$pid = $state->get("$MARKER.product");
$vars = $state->get("$MARKER.variations", []);
echo "Scike store IA — tracked state\n";
echo " pages: " . count($pages) . (count($pages) ? " (" . implode(', ', array_keys($pages)) . ")" : "") . "\n";
echo " menu links: " . count($links) . "\n";
echo " product: " . ($pid ?: '(none)') . "\n";
echo " variations: " . count($vars) . "\n";
echo " blocks: " . count($state->get("$MARKER.blocks", [])) . "\n";
echo " prev front: " . ($state->get("$MARKER.prev_front") ?: '(none)') . "\n";
return;
}
if ($mode === 'clean') {
echo "Cleaning up Scike store IA...\n";
delete_tracked($MARKER, $state, $etm);
// Restore the original front page (fall back to /user/login if it's gone).
$prev = $state->get("$MARKER.prev_front");
if ($prev) {
if (preg_match('#^/node/(\d+)$#', $prev, $mm) && !$etm->getStorage('node')->load($mm[1])) {
$prev = '/user/login';
}
\Drupal::configFactory()->getEditable('system.site')->set('page.front', $prev)->save();
echo " restored front page to $prev\n";
}
$state->delete("$MARKER.prev_front");
drupal_flush_all_caches();
echo "Done. Store IA removed.\n";
return;
}
// ---------------------------------------------------------------------------
// BUILD
// ---------------------------------------------------------------------------
echo "Building Scike store IA...\n";
// Capture the original front page once, before we delete anything.
if (!$state->get("$MARKER.prev_front")) {
$front = \Drupal::config('system.site')->get('page.front') ?: '/user/login';
$state->set("$MARKER.prev_front", $front);
}
// Remove anything we created on a previous run (makes build = regenerate).
delete_tracked($MARKER, $state, $etm);
// Remove legacy / conflicting content the IA takes ownership of:
// - any leftover Scike/VibeChat commerce products (the old seed product),
// - the default "Welcome to Scike" basic page,
// - all existing main & footer menu links.
foreach ($etm->getStorage('commerce_product')->loadMultiple() as $p) {
$t = strtolower($p->label());
if (strpos($t, 'scike') !== FALSE || strpos($t, 'vibechat') !== FALSE) {
echo " removed legacy product " . $p->id() . " ('" . $p->label() . "')\n";
$p->delete();
}
}
foreach ($etm->getStorage('node')->loadByProperties(['type' => 'page']) as $n) {
if (stripos($n->label(), 'welcome to scike') !== FALSE) {
echo " removed legacy page " . $n->id() . " ('" . $n->label() . "')\n";
$n->delete();
}
}
foreach (['main', 'footer'] as $mname) {
$existing = $etm->getStorage('menu_link_content')->loadByProperties(['menu_name' => $mname]);
if ($existing) {
$etm->getStorage('menu_link_content')->delete($existing);
echo " cleared " . count($existing) . " existing '$mname' menu link(s)\n";
}
}
// --- Pages ------------------------------------------------------------------
$page_map = [];
$front_nid = NULL;
foreach ($manifest['pages'] as $entry) {
$node = $etm->getStorage('node')->create([
'type' => 'page',
'title' => $entry['title'],
'body' => ['value' => body_html($dir, $entry), 'format' => $format],
'status' => 1,
'uid' => 1,
'path' => ['alias' => $entry['alias']],
]);
$node->save();
$page_map[$entry['key']] = $node->id();
if (($entry['key'] ?? '') === ($manifest['front_page_key'] ?? '')) {
$front_nid = $node->id();
}
echo " page '{$entry['key']}' -> /node/{$node->id()} ({$entry['alias']})\n";
}
$state->set("$MARKER.pages", $page_map);
// Front page.
if ($front_nid) {
\Drupal::configFactory()->getEditable('system.site')->set('page.front', "/node/$front_nid")->save();
echo " set front page to /node/$front_nid\n";
}
// --- Product, attribute values & variations --------------------------------
$prod = $manifest['product'];
$attr = $prod['attribute'];
$store = \Drupal::entityTypeManager()->getStorage('commerce_store')->loadDefault();
$store_id = $store ? $store->id() : 1;
// Honor the explicit variation titles from the manifest ("Scike — Starter")
// rather than Commerce's auto-generated "<product> - <attribute>" form.
$vtype = $etm->getStorage('commerce_product_variation_type')->load('default');
if ($vtype && $vtype->shouldGenerateTitle()) {
$vtype->setGenerateTitle(FALSE)->save();
echo " disabled auto title generation on 'default' variation type\n";
}
// Clear any orphaned plan attribute values (e.g. from a half-finished run)
// that match the names we're about to create, so we never duplicate them.
$wanted_names = array_map(fn($v) => $v['attr_value'], $prod['variations']);
foreach ($etm->getStorage('commerce_product_attribute_value')->loadByProperties(['attribute' => $attr]) as $av_existing) {
if (in_array($av_existing->getName(), $wanted_names, TRUE)) {
$av_existing->delete();
}
}
$attr_value_ids = [];
$variation_ids = [];
foreach ($prod['variations'] as $v) {
// Plan attribute value (e.g. Starter / Pro / Publisher).
$av = $etm->getStorage('commerce_product_attribute_value')->create([
'attribute' => $attr,
'name' => $v['attr_value'],
]);
$av->save();
$attr_value_ids[] = $av->id();
$values = [
'type' => 'default',
'sku' => $v['sku'],
'title' => $v['title'],
'price' => new Price($v['price'], $prod['currency'] ?? 'USD'),
'status' => 1,
'uid' => 1,
"attribute_$attr" => $av->id(),
];
if (!empty($prod['billing_schedule'])) {
$values['billing_schedule'] = $prod['billing_schedule'];
}
if (!empty($prod['subscription_type'])) {
// subscription_type is a commerce plugin-item field; it needs the plugin
// id wrapped, not a bare string.
$values['subscription_type'] = [
'target_plugin_id' => $prod['subscription_type'],
'target_plugin_configuration' => [],
];
}
$variation = $etm->getStorage('commerce_product_variation')->create($values);
$variation->save();
$variation_ids[] = $variation->id();
echo " variation '{$v['sku']}' -> {$v['title']} ({$v['price']})\n";
}
$product_values = [
'type' => 'default',
'title' => $prod['title'],
'stores' => [$store_id],
'variations' => $variation_ids,
'status' => 1,
'uid' => 1,
'path' => ['alias' => $prod['alias']],
];
$product = $etm->getStorage('commerce_product')->create($product_values);
// Some installs add a body field to products; set it only if present.
if ($product->hasField('body')) {
$product->set('body', ['value' => body_html($dir, $prod), 'format' => $format]);
}
$product->save();
echo " product -> /product/{$product->id()} (alias {$prod['alias']})\n";
$state->set("$MARKER.product", $product->id());
$state->set("$MARKER.variations", $variation_ids);
$state->set("$MARKER.attr_values", $attr_value_ids);
// --- Menus ------------------------------------------------------------------
$tracked_links = [];
$mk = function (string $menu, array $item, int $weight, ?string $parent) use ($etm, &$tracked_links): string {
$link = $etm->getStorage('menu_link_content')->create([
'title' => $item['title'],
'link' => ['uri' => $item['uri']],
'menu_name' => $menu,
'weight' => $weight,
'expanded' => !empty($item['children']),
'parent' => $parent,
]);
$link->save();
$tracked_links[] = $link->id();
return 'menu_link_content:' . $link->uuid();
};
foreach (($manifest['menus'] ?? []) as $menu_name => $items) {
$w = 0;
foreach ($items as $item) {
$parent_plugin = $mk($menu_name, $item, $w++, NULL);
if (!empty($item['children'])) {
$cw = 0;
foreach ($item['children'] as $child) {
$mk($menu_name, $child, $cw++, $parent_plugin);
}
}
}
echo " built '$menu_name' menu\n";
}
$state->set("$MARKER.menu_links", $tracked_links);
// --- Footer menu block ------------------------------------------------------
// The active theme renders the main menu but doesn't place a footer menu block,
// so place one (in the theme's footer region) to surface the footer nav.
$tracked_blocks = [];
if (!empty($manifest['footer_block'])) {
$fb = $manifest['footer_block'];
$theme = \Drupal::config('system.theme')->get('default');
// Remove a stale block of the same id first, then create fresh.
if ($old = $etm->getStorage('block')->load($fb['id'])) {
$old->delete();
}
$block = $etm->getStorage('block')->create([
'id' => $fb['id'],
'theme' => $theme,
'region' => $fb['region'],
'plugin' => 'system_menu_block:' . $fb['menu'],
'weight' => 0,
'settings' => [
'label' => $fb['label'] ?? 'Footer',
'label_display' => '0',
'level' => 1,
'depth' => 2,
],
]);
$block->save();
$tracked_blocks[] = $block->id();
echo " placed footer menu block '{$fb['id']}' in '{$theme}' / {$fb['region']}\n";
}
$state->set("$MARKER.blocks", $tracked_blocks);
drupal_flush_all_caches();
echo "Done. Store IA built (" . count($page_map) . " pages, " . count($tracked_links) . " menu links, 1 product / " . count($variation_ids) . " variations).\n";