diff --git a/docs/css/custom.css b/docs/css/custom.css index fe9b85528a..eae77fe84e 100644 --- a/docs/css/custom.css +++ b/docs/css/custom.css @@ -158,7 +158,7 @@ body { } .md-typeset h1 { - margin: 0 0 1rem; + margin: 0 0 0.5rem; font-size: 24px; line-height: 34px; } diff --git a/llmstxt_preprocess.py b/llmstxt_preprocess.py new file mode 100644 index 0000000000..13b6a3a810 --- /dev/null +++ b/llmstxt_preprocess.py @@ -0,0 +1,68 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from bs4 import BeautifulSoup + + +def preprocess(soup: "BeautifulSoup", output: str) -> None: + """ + Preprocess HTML to improve markdown conversion. + + Converts card macro HTML structure into markdown lists with links + so they are preserved in the llms.txt output. + + Filters out release notes filter UI elements. + """ + # Remove release notes filter UI (checkboxes and labels) + # These are interactive filters, not content + filter_containers = soup.find_all("div", class_="release-notes-filters") + for container in filter_containers: + container.decompose() + + # Find all cards wrapper divs (these contain groups of cards) + cards_divs = soup.find_all("div", class_=lambda c: c and c.startswith("cards ")) + + for cards_div in cards_divs: + # Find all card-wrapper divs within this cards group + card_wrappers = cards_div.find_all("div", class_="card-wrapper") + + if not card_wrappers: + continue + + # Create a list to hold all the cards in this group + ul = soup.new_tag("ul") + + for card_wrapper in card_wrappers: + # Extract the link, title, and description from the card structure + link = card_wrapper.find("a", class_="card") + if not link: + continue + + href = link.get("href", "") + # Fix protocol-relative URLs + if href.startswith("//"): + href = "https:" + href + + title_elem = link.find("p", class_="title") + description_elem = link.find("p", class_="description") + + if not title_elem: + continue + + title = title_elem.get_text(strip=True) + description = description_elem.get_text(strip=True) if description_elem else "" + + # Create a list item with a link and description + li = soup.new_tag("li") + link_tag = soup.new_tag("a", href=href) + link_tag.string = title + li.append(link_tag) + + if description: + li.append(soup.new_string(" - ")) + li.append(soup.new_string(description)) + + ul.append(li) + + # Replace the entire cards div with the unordered list + cards_div.replace_with(ul) diff --git a/mkdocs.yml b/mkdocs.yml index a2082cc594..2f30560198 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -2,6 +2,7 @@ INHERIT: plugins.yml site_name: Developer Documentation repo_url: https://github.com/ibexa/documentation-developer +edit_uri: edit/5.0/docs/ site_url: https://doc.ibexa.co/en/latest/ copyright: "Copyright 1999-2026 Ibexa AS and others" validation: diff --git a/plugins.yml b/plugins.yml index 77c3fd4879..bcc5d4d049 100644 --- a/plugins.yml +++ b/plugins.yml @@ -586,3 +586,158 @@ plugins: 'ai_actions/install_ai_actions.md': 'ai_actions/configure_ai_actions.md' 'discounts/install_discounts.md': 'discounts/configure_discounts.md' 'content_management/collaborative_editing/install_collaborative_editing.md': 'content_management/collaborative_editing/configure_collaborative_editing.md' + - llmstxt: + preprocess: llmstxt_preprocess.py + markdown_description: | + > Ibexa DXP developer documentation - comprehensive technical guide for building digital experiences with Ibexa, a modern PHP-based Digital Experience Platform built on Symfony Full Stack Framework. + Ibexa is an enterprise-grade content management and digital commerce platform that comes in three editions: Headless (API-first content management), Experience (full DXP with Page Builder and forms), and Commerce (complete e-commerce solution). + Key technical capabilities include: flexible content modeling with custom field types, powerful REST and GraphQL APIs, headless and traditional architecture support, Product Information Management (PIM), integrated commerce features (cart, checkout, payment, shipping), multi-site and multi-language support, advanced search with Elasticsearch/Solr, customizable Twig templating, Symfony-based extensibility, workflow management, AI Actions for content generation, Form Builder, and comprehensive user/permission management. + The platform is designed for developers to build scalable content-driven applications, e-commerce sites, and headless solutions with enterprise features like Customer Data Platform (CDP), personalization, editorial workflows, and extensive APIs. Built with modern PHP practices on Symfony 7.x, supporting MySQL/MariaDB and PostgreSQL databases. + full_output: llms-full.txt + sections: + Ibexa Developer Documentation: + - index.md + Ibexa DXP editions: + - ibexa_products/*.md + Getting started: + - getting_started/*.md + Tutorials: + - tutorials/tutorials.md + - tutorials/beginner_tutorial/*.md + - tutorials/generic_field_type/*.md + - tutorials/page_and_form_tutorial/*.md + API: + - api/api.md + - api/event_reference/*.md + - api/graphql/*.md + - api/php_api/php_api.md + - api/rest_api/rest_api_authentication.md + - api/rest_api/extending_rest_api/*.md + - api/rest_api/rest_api_usage/*.md + Administration: + - administration/administration.md + - administration/admin_panel/*.md + - administration/back_office/*.md + - administration/back_office/back_office_elements/*.md + - administration/back_office/back_office_menus/*.md + - administration/back_office/back_office_tabs/*.md + - administration/back_office/browser/*.md + - administration/configuration/*.md + - administration/content_organization/*.md + - administration/dashboard/*.md + - administration/project_organization/*.md + - administration/recent_activity/recent_activity.md + Content management: + - content_management/*.md + - content_management/collaborative_editing/*.md + - content_management/content_api/*.md + - content_management/content_management_api/*.md + - content_management/data_migration/*.md + - content_management/field_types/*.md + - content_management/field_types/field_type_reference/*.md + - content_management/file_management/*.md + - content_management/forms/*.md + - content_management/images/*.md + - content_management/pages/*.md + - content_management/rich_text/*.md + - content_management/taxonomy/*.md + - content_management/url_management/*.md + - content_management/workflow/*.md + Templating: + - templating/*.md + - templating/design_engine/*.md + - templating/embed_and_list_content/*.md + - templating/layout/*.md + - templating/queries_and_controllers/*.md + - templating/render_content/*.md + - templating/templates/*.md + - templating/twig_function_reference/*.md + - templating/urls_and_routes/*.md + AI Actions: + - ai_actions/*.md + PIM (Product management): + - pim/*.md + - pim/attributes/*.md + Commerce: + - commerce/commerce.md + - commerce/cart/*.md + - commerce/checkout/*.md + - commerce/order_management/*.md + - commerce/payment/*.md + - commerce/shipping_management/*.md + - commerce/storefront/*.md + - commerce/transactional_emails/*.md + Discounts: + - discounts/*.md + Customer management: + - customer_management/*.md + Ibexa Engage: + - ibexa_engage/*.md + Multisite: + - multisite/*.md + - multisite/languages/*.md + - multisite/site_factory/*.md + - multisite/siteaccess/*.md + Permissions: + - permissions/*.md + Users: + - users/*.md + CDP (Customer Data Platform): + - cdp/*.md + - cdp/cdp_activation/*.md + Search: + - search/*.md + - search/activity_log_search_reference/*.md + - search/aggregation_reference/*.md + - search/ai_actions_search_reference/*.md + - search/collaboration_search_reference/*.md + - search/content_type_search_reference/*.md + - search/criteria_reference/*.md + - search/discounts_search_reference/*.md + - search/extensibility/*.md + - search/search_engines/search_engines.md + - search/search_engines/elasticsearch/*.md + - search/search_engines/legacy_search_engine/*.md + - search/search_engines/solr_search_engine/*.md + - search/sort_clause_reference/*.md + - search/url_search_reference/*.md + Ibexa Cloud: + - ibexa_cloud/*.md + Infrastructure and maintenance: + - infrastructure_and_maintenance/*.md + - infrastructure_and_maintenance/cache/*.md + - infrastructure_and_maintenance/cache/http_cache/*.md + - infrastructure_and_maintenance/clustering/*.md + - infrastructure_and_maintenance/security/*.md + Update and migration: + - update_and_migration/update_ibexa_dxp.md + - update_and_migration/from_4.0/to_4.1.md + - update_and_migration/from_4.1/update_from_4.1.md + - update_and_migration/from_4.2/update_from_4.2.md + - update_and_migration/from_4.3/*.md + - update_and_migration/from_4.4/update_from_4.4.md + - update_and_migration/from_4.5/update_from_4.5.md + - update_and_migration/from_4.6/*.md + - update_and_migration/from_5.0/update_from_5.0.md + - update_and_migration/migrate_to_ibexa_dxp/*.md + Resources: + - resources/*.md + - resources/contributing/*.md + Product guides: + - product_guides/product_guides.md + Release notes: + - release_notes/release_notes.md + - release_notes/ibexa_dxp_v5.0.md + - release_notes/ibexa_dxp_v5.0_deprecations.md + - release_notes/ibexa_dxp_v4.6.md + - release_notes/ibexa_dxp_v4.5.md + - release_notes/ibexa_dxp_v4.4.md + - release_notes/ibexa_dxp_v4.3.md + - release_notes/ibexa_dxp_v4.2.md + - release_notes/ibexa_dxp_v4.1.md + - release_notes/ibexa_dxp_v4.0.md + PHP API Reference - Signatures: + - api/php_api/php_api_reference.md + - api/php_api/php_api_signatures.md + REST API Reference: + - api/rest_api/rest_api_reference_overview.md diff --git a/requirements.txt b/requirements.txt index c03e3d1a35..688d9446a1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,3 +7,4 @@ mkdocs-macros-plugin==1.3.7 mkdocs-redirects==1.2.2 mkdocs-autolinks-plugin==0.7.1 Jinja2==3.1.6 +mkdocs-llmstxt diff --git a/theme/assets/page-actions.js b/theme/assets/page-actions.js new file mode 100644 index 0000000000..6323042755 --- /dev/null +++ b/theme/assets/page-actions.js @@ -0,0 +1,103 @@ +/** + * Page Actions JavaScript + * Handles functionality for page action buttons (Copy for LLM, View as Markdown, Edit on GitHub) + */ + +async function copyPageForLLM() { + try { + const mdPath = document.querySelector('meta[name="markdown-path"]').content; + console.log('Fetching from path:', mdPath); + + let markdownContent; + + try { + const response = await fetch(mdPath); + if (response.ok) { + markdownContent = await response.text(); + } else { + throw new Error(`HTTP error! status: ${response.status}`); + } + } catch (fetchError) { + console.log('Direct fetch failed, trying GitHub fallback...'); + const editUrl = document.querySelector('meta[name="edit-url"]').content; + const rawUrl = editUrl.replace('/edit/', '/raw/'); + + try { + const response = await fetch(rawUrl); + if (response.ok) { + markdownContent = await response.text(); + } else { + const proxyUrl = `https://api.allorigins.win/get?url=${encodeURIComponent(rawUrl)}`; + const proxyResponse = await fetch(proxyUrl); + if (proxyResponse.ok) { + const proxyData = await proxyResponse.json(); + markdownContent = proxyData.contents; + } else { + throw new Error('All fetch methods failed'); + } + } + } catch (githubError) { + throw new Error('GitHub fallback failed'); + } + } + + if (!markdownContent) { + throw new Error('No content received'); + } + + await navigator.clipboard.writeText(markdownContent); + showButtonFeedback('success', 'Copied!', '📋'); + + } catch (error) { + console.error('Failed to copy content:', error); + + try { + const mdPath = document.querySelector('meta[name="markdown-path"]').content; + window.open(mdPath, '_blank'); + showButtonFeedback('info', 'Opened in tab', '🔗'); + } catch (fallbackError) { + showButtonFeedback('error', 'Failed', '❌'); + } + } +} + +function showButtonFeedback(type, message, icon) { + const button = document.querySelector('button[onclick="copyPageForLLM()"]'); + if (!button) return; + + const originalHTML = button.innerHTML; + button.innerHTML = `${icon} ${message}`; + + if (type === 'success') { + button.style.background = '#d4edda'; + button.style.borderColor = '#c3e6cb'; + button.style.color = '#155724'; + } else if (type === 'error') { + button.style.background = '#f8d7da'; + button.style.borderColor = '#f5c6cb'; + button.style.color = '#721c24'; + } else if (type === 'info') { + button.style.background = '#d1ecf1'; + button.style.borderColor = '#bee5eb'; + button.style.color = '#0c5460'; + } + + setTimeout(() => { + button.innerHTML = originalHTML; + button.style.background = ''; + button.style.borderColor = ''; + button.style.color = ''; + }, 2000); +} + +document.addEventListener('DOMContentLoaded', function() { + console.log('Page actions initialized'); + + const pageActions = document.getElementById('page-actions'); + const firstH1 = document.querySelector('.bootstrap-iso h1, h1'); + + if (pageActions && firstH1) { + firstH1.insertAdjacentElement('afterend', pageActions); + pageActions.style.display = 'flex'; + } +}); diff --git a/theme/main.html b/theme/main.html index 66ce7dc87d..110fbcaf1a 100644 --- a/theme/main.html +++ b/theme/main.html @@ -13,6 +13,93 @@ + + + + + + {% if config.repo_url and page.edit_url %} + + + {% endif %} + + + {% endblock %} {% block site_nav %} {% if nav %} @@ -75,7 +162,36 @@ {% endif %} {% include "partials/eol_warning.html" %} - {{ page.content }} + +
+ {{ page.content }} + + + {% if config.repo_url and page.edit_url and not page.is_homepage and 'index.md' not in page.file.src_path %} +
+ + + + + + + View as Markdown + + + + + + Edit on GitHub + +
+ {% endif %} +
+ {% include "partials/tags.html" %} {% endblock %} diff --git a/theme/partials/header.html b/theme/partials/header.html index ba5ca0931d..80a4345297 100644 --- a/theme/partials/header.html +++ b/theme/partials/header.html @@ -20,10 +20,5 @@ {% include ".icons/material/magnify.svg" %} {% include "partials/search.html" %} - {% if config.repo_url %} -
- {% include "partials/source.html" %} -
- {% endif %} diff --git a/theme/partials/source.html b/theme/partials/source.html deleted file mode 100644 index 76e885dc14..0000000000 --- a/theme/partials/source.html +++ /dev/null @@ -1,7 +0,0 @@ -{% import "partials/language.html" as lang with context %} - -
- {% include ".icons/fontawesome/brands/github-alt.svg" %} -
- View on GitHub -
diff --git a/update_llmstxt_config.py b/update_llmstxt_config.py new file mode 100755 index 0000000000..20196c4405 --- /dev/null +++ b/update_llmstxt_config.py @@ -0,0 +1,367 @@ +#!/usr/bin/env python3 +""" +Update the llmstxt plugin configuration in plugins.yml based on mkdocs.yml nav structure. +This script converts the mkdocs navigation into a format suitable for the llmstxt plugin, +using glob patterns where possible to simplify the configuration. +""" + +import yaml +from pathlib import Path +from collections import defaultdict + + +def group_files_by_directory(files): + """ + Group files by their directory and check if they can be represented by glob patterns. + Returns a dict mapping directory paths to file lists. + """ + files_by_dir = defaultdict(list) + + for file in files: + if isinstance(file, str): + file_path = Path(file) + directory = str(file_path.parent) + files_by_dir[directory].append(file) + + return files_by_dir + + +def convert_to_glob_patterns(files): + """ + Convert a list of files to glob patterns where appropriate. + Returns a list that may contain glob patterns or individual files. + + Strategy: + - If multiple files from same directory: use glob pattern for that directory + - If single file or files from different dirs: list individually + - Exception: Don't glob release_notes directory (we filter by version) + """ + if not files: + return [] + + # Group by directory + files_by_dir = group_files_by_directory(files) + + result = [] + processed_dirs = set() + + # For each directory, decide whether to use glob or list files + for directory, dir_files in sorted(files_by_dir.items()): + if directory in processed_dirs: + continue + + # Don't use glob for release_notes - we want to filter by version + if 'release_notes' in directory: + result.extend(dir_files) + processed_dirs.add(directory) + continue + + # Use glob if we have 2+ markdown files in the same directory + if len(dir_files) >= 2 and all(Path(f).suffix == '.md' for f in dir_files): + if directory: # Not root + result.append(f"{directory}/*.md") + else: + result.append("*.md") + processed_dirs.add(directory) + else: + # Add files individually + result.extend(dir_files) + + return result + + +def process_nav_section(nav_item): + """ + Process a nav section and return a dict/list structure for llmstxt config. + Preserves the hierarchical structure from mkdocs.yml and uses glob patterns where possible. + """ + if isinstance(nav_item, str): + # Direct file reference + return nav_item + + if isinstance(nav_item, dict): + result = {} + for key, value in nav_item.items(): + # Skip excluded sections + if should_exclude_section(key): + continue + if isinstance(value, str): + # Single file under this section + result[key] = [value] + elif isinstance(value, list): + # Process list items + has_nested_sections = any(isinstance(item, dict) for item in value) + + if has_nested_sections: + # Check if all nested sections are simple file mappings from same directory + # If so, we can use a glob instead + all_files = [] + all_simple = True + + for item in value: + if isinstance(item, str): + all_files.append(item) + elif isinstance(item, dict): + # Check if this is a simple single-file mapping + if len(item) == 1: + item_key, item_value = next(iter(item.items())) + if isinstance(item_value, str): + all_files.append(item_value) + elif isinstance(item_value, list) and len(item_value) == 1: + all_files.append(item_value[0]) + else: + all_simple = False + break + else: + all_simple = False + break + + # If all items are from same directory, use glob + if all_simple and all_files: + glob_patterns = convert_to_glob_patterns(all_files) + # If we got a single glob pattern, use it directly + if len(glob_patterns) == 1 and '*.md' in glob_patterns[0]: + result[key] = glob_patterns + else: + # Otherwise, process normally + nested_result = {} + direct_files = [] + + for item in value: + if isinstance(item, str): + direct_files.append(item) + elif isinstance(item, dict): + processed = process_nav_section(item) + if isinstance(processed, dict): + nested_result.update(processed) + + # Convert direct files to globs where possible + if direct_files: + direct_files = convert_to_glob_patterns(direct_files) + + # If we have both direct files and nested sections + if direct_files and nested_result: + result[key] = direct_files + result.update(nested_result) + elif nested_result: + result[key] = nested_result + elif direct_files: + result[key] = direct_files + else: + # Contains nested sections - recurse normally + nested_result = {} + direct_files = [] + + for item in value: + if isinstance(item, str): + direct_files.append(item) + elif isinstance(item, dict): + processed = process_nav_section(item) + if isinstance(processed, dict): + nested_result.update(processed) + + # Convert direct files to globs where possible + if direct_files: + direct_files = convert_to_glob_patterns(direct_files) + + # If we have both direct files and nested sections + if direct_files and nested_result: + result[key] = direct_files + result.update(nested_result) + elif nested_result: + result[key] = nested_result + elif direct_files: + result[key] = direct_files + else: + # Only contains direct file references + files = [item for item in value if isinstance(item, str)] + if files: + # Convert to glob patterns where possible + glob_patterns = convert_to_glob_patterns(files) + result[key] = glob_patterns + + return result + + return None + + +def should_exclude_file(file_path): + """ + Check if a file should be excluded based on its path or name. + Excludes old version release notes and update documentation (pre-v4). + """ + # Patterns to exclude from release notes and updates + old_version_file_patterns = [ + # Release notes for v3.3 and older + 'ez_platform_v3.3', 'ez_platform_v3.2', 'ez_platform_v3.1', 'ez_platform_v3.0', + 'ez_platform_v2.5', 'ez_platform_v2.4', 'ez_platform_v2.3', 'ez_platform_v2.2', + 'ez_platform_v2.1', 'ez_platform_v2.0', + 'ez_platform_v1.13', 'ez_platform_v1.12', 'ez_platform_v1.11', 'ez_platform_v1.10', + 'ez_platform_v1.9', 'ez_platform_v1.8', 'ez_platform_v1.7', + 'ibexa_dxp_v3.3', 'ibexa_dxp_v3.2', + # Deprecations for old versions + 'ez_platform_v3.0_deprecations', + 'ibexa_dxp_v4.0_deprecations', + # Update guides for old versions (pre-v4) + 'from_1.x_2.x/', 'from_2.5/', 'from_3.3/', + ] + + for pattern in old_version_file_patterns: + if pattern in file_path: + return True + + return False + + +def should_exclude_section(section_name): + """ + Check if a section should be excluded based on exclusion rules. + + Exclusions: + - Personalization + - Update and release notes for versions older than v4 (3.3 and lower) + - v4.0 deprecations + """ + # Exclude Personalization + if 'Personalization' in section_name or 'personalization' in section_name.lower(): + return True + + # Exclude v4.0 deprecations (but keep v5.0 deprecations) + if 'v4.0 deprecations' in section_name or 'v4.0_deprecations' in section_name: + return True + + # Exclude old version updates and releases (v3.3 and lower, v2.x, v1.x) + old_version_patterns = [ + # Release notes for old versions + 'v3.3 LTS', 'v3.2', 'v3.1', 'v3.0', + 'v2.5', 'v2.4', 'v2.3', 'v2.2', 'v2.1', 'v2.0', + 'v1.13', 'v1.12', 'v1.11', 'v1.10', 'v1.9', 'v1.8', 'v1.7', + # Update sections + 'from v1.13', 'from v2.', 'from 1.x', 'from 2.x', + 'Update from v1.13', 'Update from v2.5', 'Update from v3.3', + # eZ Platform versions (all are pre-v4) + 'eZ Platform', 'ez Platform' + ] + + for pattern in old_version_patterns: + if pattern in section_name: + return True + + return False + + +def convert_nav_to_llmstxt_sections(nav_list): + """ + Convert mkdocs nav list to llmstxt sections format. + The llmstxt plugin expects a dict where each value is a list of file paths. + Uses glob patterns where possible to simplify the configuration. + Applies exclusion filters for certain sections. + """ + sections = {} + + def extract_files(item): + """Recursively extract file paths from nav structure.""" + files = [] + if isinstance(item, str): + # Skip HTML files (external references) and excluded files + if not item.endswith('.html') and not should_exclude_file(item): + files.append(item) + elif isinstance(item, list): + for subitem in item: + files.extend(extract_files(subitem)) + elif isinstance(item, dict): + for key, value in item.items(): + # Don't filter nested sections - only filter at top level + if isinstance(value, str): + # Skip HTML files (external references) and excluded files + if not value.endswith('.html') and not should_exclude_file(value): + files.append(value) + elif isinstance(value, list): + for subitem in value: + files.extend(extract_files(subitem)) + return files + + for item in nav_list: + if isinstance(item, dict): + for section_name, section_content in item.items(): + # Skip excluded sections (only at top level) + if should_exclude_section(section_name): + continue + + # Extract all files from this section + files = [] + if isinstance(section_content, str): + files.append(section_content) + elif isinstance(section_content, list): + files.extend(extract_files(section_content)) + + if files: + # Convert to glob patterns where appropriate + glob_patterns = convert_to_glob_patterns(files) + sections[section_name] = glob_patterns + elif isinstance(item, str): + # Top-level file + if 'Ibexa Developer Documentation' not in sections: + sections['Ibexa Developer Documentation'] = [] + sections['Ibexa Developer Documentation'].append(item) + + return sections + + +def update_plugins_yml(plugins_path, mkdocs_path): + """ + Update the llmstxt plugin configuration in plugins.yml based on mkdocs.yml nav. + """ + # Read both files + with open(plugins_path, 'r') as f: + plugins_data = yaml.safe_load(f) + + with open(mkdocs_path, 'r') as f: + mkdocs_data = yaml.safe_load(f) + + # Convert nav to sections + nav = mkdocs_data.get('nav', []) + new_sections = convert_nav_to_llmstxt_sections(nav) + + # Find and update llmstxt plugin + plugins_list = plugins_data.get('plugins', []) + for i, plugin in enumerate(plugins_list): + if isinstance(plugin, dict) and 'llmstxt' in plugin: + # Update sections while preserving other settings + plugin['llmstxt']['sections'] = new_sections + print(f"✓ Updated llmstxt plugin configuration") + print(f" Total sections: {len(new_sections)}") + break + else: + print("✗ llmstxt plugin not found in plugins.yml") + return False + + # Write back to file + with open(plugins_path, 'w') as f: + yaml.dump(plugins_data, f, default_flow_style=False, sort_keys=False, + allow_unicode=True, width=120) + + print(f"✓ Updated {plugins_path}") + return True + + +if __name__ == '__main__': + script_dir = Path(__file__).parent + plugins_path = script_dir / 'plugins.yml' + mkdocs_path = script_dir / 'mkdocs.yml' + + if not plugins_path.exists(): + print(f"✗ plugins.yml not found at {plugins_path}") + exit(1) + + if not mkdocs_path.exists(): + print(f"✗ mkdocs.yml not found at {mkdocs_path}") + exit(1) + + print("Updating llmstxt configuration...") + print(f"Reading from: {mkdocs_path}") + print(f"Updating: {plugins_path}") + print() + + success = update_plugins_yml(plugins_path, mkdocs_path) + exit(0 if success else 1)