diff --git a/doc/zh/Command.md b/doc/zh/Command.md index f98ee7a1b26e413e07d7ca6746d42c29fb4199d8..7a0e5b52dd15c6cb5eeb30f8a7f93945bc6ccc91 100644 --- a/doc/zh/Command.md +++ b/doc/zh/Command.md @@ -8,7 +8,7 @@ ## `oedp info` -查看项目的详细信息,默认为当前路径。 +查看项目的详细信息,默认为当前路径 | 选项 | 简写 | 是否必需 | 功能说明 | | -------------------- | ------ | -------- | ------------------------ | @@ -16,7 +16,7 @@ ## `oedp run [action]` -执行项目某个方法,默认为当前路径;`[action]`为插件支持的操作,可通过 `oedp info`命令查询 +执行项目某个方法,默认为当前路径;`[action]`为插件可使用的方法,可通过 `oedp info`命令查询 | 选项 | 简写 | 是否必需 | 功能说明 | | ------------------ | ---- | -------- | ------------------------------------ | @@ -27,23 +27,23 @@ ## `oedp list` -列举插件源中可用的插件。 +列举插件源中可用的插件 ## `oedp init [plugin]` -插件初始化到指定路径,`[plugin]`可以是插件压缩包路径、插件下载地址、插件名称。 +插件初始化到指定路径,`[plugin]`可以是插件压缩包路径、插件下载地址、插件名称 -- `[plugin]`如果为本地的插件压缩包(以`tar.gz`结尾),则直接初始化到指定路径。 -- `[plugin]`如果为插件下载地址(以`tar.gz`结尾),则先下载到缓存路径`/var/oedp/plugin/`,再初始化到指定路径。 -- `[plugin]`如果为插件名称,从已经配置的插件源中查找,下载到缓存路径后初始化到指定路径。 +- `[plugin]`如果为本地的插件压缩包(以`tar.gz`结尾),则直接初始化到指定路径 +- `[plugin]`如果为插件下载地址(以`tar.gz`结尾),则先下载到缓存路径`/var/oedp/plugin/`,再初始化到指定路径 +- `[plugin]`如果为插件名称,从已经配置的插件源中查找,下载到缓存路径后初始化到指定路径 | 选项 | 简写 | 是否必需 | 功能说明 | | ------------------ | ---- | -------- | ------------------------------------------------------------ | | `--project [path]` | `-p` | N | 项目路径,若不存在则创建 | -| `--dir [path]` | `-d` | N | 项目的父路径,若不存在则创建。如果未指定项目路径与父路径,则父路径默认取当前目录 | +| `--dir [path]` | `-d` | N | 项目的父路径,若不存在则创建。如果未指定项目路径与父路径,则父路径默认取当前目录。 | | `--force` | `-f` | N | 强制覆盖路径,请谨慎使用;如果路径存在,会先删除该路径中的所有文件,再初始化 | -示例:假设当前路径为家目录`~`,如下5个命令执行后,都是初始化了一个目录为`~/kubernetes-1.31.1`的 oeDeploy 插件。 +示例:假设当前路径为家目录`~`,如下5个命令执行后,都是初始化了一个目录为`~/kubernetes-1.31.1`的 oeDeploy 插件 ````bash oedp init kubernetes-1.31.1 @@ -69,7 +69,7 @@ oedp init kubernetes-1.31.1 -d ~ | `enable [name]` | 使能插件源 | | `disable [name]` | 去使能插件源 | -插件源配置文件 `/etc/oedp/config/repo/repo.conf`示例。 +插件源配置文件 `/etc/oedp/config/repo/repo.conf`示例 ````ini [openEuler] @@ -87,6 +87,7 @@ enabled = false | --------------------------------- | ------------------------------------------------------------ | | `/etc/oedp/config/` | 配置文件路径 | | `/etc/oedp/config/repo/cache/` | 插件源索引文件缓存路径,每个已经生效的插件源都会生成一个`.yaml`文件 | +| `/etc/oedp/config/repo/details` | 插件配置信息缓存目录(每个插件删除`workspace`后的部分),用于 DevStore 读取图标与 README。 | | `/etc/oedp/config/repo/repo.conf` | 插件源配置文件 | | `/usr/lib/oedp/src/` | 源码路径 | | `/var/oedp/log/` | 日志文件路径 | @@ -94,7 +95,7 @@ enabled = false # # oeDeploy 插件源 -## 插件源格式说明 +## 插件源格式说明 oeDeploy 支持从可访问的插件源自动获取安装部署插件,并初始化到本地。 @@ -105,36 +106,42 @@ oeDeploy 提供了脚本 `tools/make_repo_index/make_repo_index.py`,用于自 `index.yaml`示例如下: ```yaml ---- apiversion: v1 plugins: -- kubernetes-1.31.1: - - name: kubernetes-1.31.1 - version: 1.0.0-1 - updated: "2025-03-05T10:31:02.608017752+08:00" - description: oeDeploy plugin for kubernetes deployment - icon: https://gitee.com/openeuler/oeDeploy/blob/master/oedp/build/static/oeDeploy.png +- anythingLLM: + - name: anythingLLM + version: 1.0.0 + updated: 2025-07-26T17:43:28+0800 + author: '' + url: https://gitee.com/openeuler/oeDeploy/tree/master/plugins/anythingLLM + description: oeDeploy plugin for anythingLLM fast deployment + description_zh: 用于anythingLLM快速部署的oeDeploy插件 + description_en: oeDeploy plugin for anythingLLM fast deployment + readme: '' + icon: '' + localhost_available: true type: app - sha256sum: 683995d14bbf425f18d1086858f210fc704b1d14edb274382d0e518a5d2a92c1 - size: 1061949301 + sha256sum: 54f22780afadd02559e4a5bfdf672c6f2af1f17a73fe08fc4e3cd82aeee0cdef + size: 12820 urls: - - https://repo.oepkgs.net/openEuler/rpm/openEuler-24.03-LTS/contrib/oedp/plugins/kubernetes-1.31.1.tar.gz - - name: kubernetes-1.31.1 - version: 1.0.0-2 - ... -- pytorch: - - name: pytorch - version: 1.0.0-1 - updated: "2025-03-05T10:31:02.608017752+08:00" - description: oeDeploy plugin for pytorch deployment - icon: https://gitee.com/openeuler/oeDeploy/blob/master/oedp/build/static/oeDeploy.png + - https:/repo.oepkgs.net/openEuler/rpm/openEuler-24.03-LTS/contrib/oedp/plugins/anythingLLM.tar.gz +- deepseek-r1: + - name: deepseek-r1 + version: 1.0.0 + updated: 2025-05-27T21:33:00+0800 + author: '' + url: '' + description: deepseek-r1 1.0.0 + description_zh: '' + description_en: '' + readme: '' + icon: '' + localhost_available: false type: app - sha256sum: 3fe0cb97e01ac9af2c1c8d12d12753f82988ab0e39b5878f359829574102c79d - size: 2373 + sha256sum: 9c991734052aa32c2ca362835115c6f2cb51026226f0aecbfeda134348812408 + size: 4787 urls: - - https://repo.oepkgs.net/openEuler/rpm/openEuler-24.03-LTS/contrib/oedp/plugins/pytorch.tar.gz - ... -... + - https:/repo.oepkgs.net/openEuler/rpm/openEuler-24.03-LTS/contrib/oedp/plugins/deepseek-r1.tar.gz ``` 首层字段含义: @@ -146,38 +153,49 @@ plugins: 插件字段含义: -| 字段 | 含义 | 获取方式 | -| ------------- | ------------------------------------------------------------ | ------------------------------- | -| `name` | 插件名称。允许插件名称中带有版本号,表示软件本身的版本,而非插件的版本。 | 与插件压缩包同名(不带.tar.gz) | -| `version` | 插件版本号,并非所部署软件的版本号。 | 读取main.yaml | -| `updated` | 插件更新的时间,格式为`%Y-%m-%dT%H:%M:%S%z`。允许为空。 | 插件压缩包最后修改时间 | -| `description` | 插件介绍。允许为空。 | 读取main.yaml | -| `icon` | 插件图标地址,用于Web端显示。允许为空。 | 读取main.yaml | -| `type` | 插件类型。保留字段,暂不生效。默认值 `app`。 | 读取main.yaml | -| `sha256sum` | 插件文件sha256数值,用于下载时的完整性校验。 | sha256计算 | -| `size` | 插件文件大小,单位Bytes。 | 压缩包实际大小 | -| `urls` | 插件下载地址,可以有多个。所有url地址都必须在当前插件源目录的范围内。 | 前缀与相对路径的拼接结果 | +| 字段 | 含义 | 获取方式 | +| --------------------- | ------------------------------------------------------------ | ------------------------------- | +| `name` | 插件名称。允许插件名称中带有版本号,表示软件本身的版本,而非插件的版本。 | 与插件压缩包同名(不带.tar.gz) | +| `version` | 插件版本号,并非所部署软件的版本号。 | 读取main.yaml | +| `updated` | 插件更新的时间,格式为`%Y-%m-%dT%H:%M:%S%z`。允许为空。 | 插件打包时间 | +| `author` | 插件发布者。允许为空。 | 读取main.yaml | +| `description` | 插件简介(当多语言简介缺失时,作为缺省值)。允许为空。 | 读取main.yaml | +| `description_zh` | 插件简介(中文,用于检索匹配)。允许为空。 | 读取main.yaml | +| `description_en` | 插件简介(英文,用于检索匹配)。允许为空。 | 读取main.yaml | +| `icon` | 插件图标地址,用于Web端显示。允许为空。 | 读取main.yaml | +| `type` | 插件类型。保留字段,暂不生效。默认值 `app`。 | 读取main.yaml | +| `localhost_available` | 插件是否支持本地单节点部署。允许为空,缺省为`false`。 | 读取main.yaml | +| `sha256sum` | 插件文件sha256数值,用于下载时的完整性校验。 | sha256计算 | +| `size` | 插件文件大小,单位Bytes。 | 压缩包实际大小 | +| `download_urls` | 插件下载地址,可以有多个。所有url地址都必须在当前插件源目录的范围内。 | 前缀与相对路径的拼接结果 | ## 插件源构建方式 执行如下命令,一键生成索引文件 `index.yaml`: -````bash -python3 make_repo_index.py [plugins_dir] [url_prefix] [output_dir] -```` +```bash +python3 make_repo_index.py [plugins_dir] [url_prefix] [output_dir] [extra_item] +``` - `plugins_dir`表示当前 Linux 环境下的插件源根目录,脚本会自动识别目录下(包括所有子目录)所有符合条件的 oeDeploy 插件(后缀为.tar.gz的压缩包)。 - oeDeploy 插件合法性的判断标准是:.tar.gz文件中根目录下包含`main.yaml`、`config.yaml`两个文件和`workspace`目录。 - `name`、`version`、`updated`、`description`、`icon`、`type`信息都从`main.yaml`中读取,如果未读取到,则字段可以缺失。 - `url_prefix`表示每个插件源的 url 索引前缀,即对外暴露的插件源根目录。 - `output_dir`表示索引文件 `index.yaml`输出的目录。 +- `extra_item`(可选)表示额外补充项的YAML文件路径,用于合并额外的插件版本信息到索引中。 + +脚本会自动处理以下情况: +1. 合并主目录和额外文件中的插件版本 +2. 按版本号从大到小排序 +3. 自动去重相同插件名和版本号的条目 + 例如,当前 Linux 环境上 `/root/build_workspace/storages/plugins`目录中有多个 oeDeploy 插件,同时映射到 `http://x.x.x.x:8080/plugins`,作为文件服务器对外暴露。 +未存储在当前环境的oeDeploy插件信息记录在 `/root/build_workspace/storages/extra.yaml`。 + 执行如下命令,可以在 `/root/build_workspace/storages/plugins`目录下生成一个 `index.yaml`,其中每个插件的 `urls`字段是其在文件服务器上的真实访问地址,可以直接下载。 -````bash -python3 make_repo_index.py /root/build_workspace/storages/plugins http://x.x.x.x:8080/plugins /root/build_workspace/storages/plugins -```` +```bash +python3 make_repo_index.py /root/build_workspace/storages/plugins http://x.x.x.x:8080/plugins /root/build_workspace/storages/plugins /root/build_workspace/storages/extra.yaml +``` \ No newline at end of file diff --git a/tools/make_repo_index/README.md b/tools/make_repo_index/README.md index ad03539fdd0d3ae9546cb88448cd8015574ea62d..e8375f05bbefce01fadf8f7afa2d602e25053a75 100644 --- a/tools/make_repo_index/README.md +++ b/tools/make_repo_index/README.md @@ -3,7 +3,7 @@ 执行如下命令,一键生成索引文件 `index.yaml`: ```bash -python3 make_repo_index.py [plugins_dir] [url_prefix] [output_dir] +python3 make_repo_index.py [plugins_dir] [url_prefix] [output_dir] [extra_item] ``` - `plugins_dir`表示当前 Linux 环境下的插件源根目录,脚本会自动识别目录下(包括所有子目录)所有符合条件的 oeDeploy 插件(后缀为.tar.gz的压缩包)。 @@ -12,10 +12,39 @@ python3 make_repo_index.py [plugins_dir] [url_prefix] [output_dir] - `output_dir`表示索引文件 `index.yaml`输出的目录。 +- `extra_item`(可选)表示额外补充项的YAML文件路径,用于合并额外的插件版本信息到索引中。文件格式示例: + ```yaml + apiversion: v1 + plugins: + - anythingLLM: + - name: anythingLLM + version: 1.0.1 + updated: 2025-05-15T19:32:52+0800 + author: '' + url: '' + description: anythingLLM + description_zh: '' + description_en: '' + readme: '' + icon: '' + type: app + sha256sum: 1be1169f28e02781f2ad2fb6ff732e98d7704a9e097dbfcc25e8480acbc20bbb + size: 3861 + urls: + - https:/repo.oepkgs.net/openEuler/rpm/openEuler-24.03-LTS/contrib/oedp/plugins/anythingLLM.tar.gz + ``` + +脚本会自动处理以下情况: +1. 合并主目录和额外文件中的插件版本 +2. 按版本号从大到小排序 +3. 自动去重相同插件名和版本号的条目 + 例如,当前 Linux 环境上 `/root/build_workspace/storages/plugins`目录中有多个 oeDeploy 插件,同时映射到 `http://x.x.x.x:8080/plugins`,作为文件服务器对外暴露。 +未存储在当前环境的oeDeploy插件信息记录在 `/root/build_workspace/storages/extra.yaml`。 + 执行如下命令,可以在 `/root/build_workspace/storages/plugins`目录下生成一个 `index.yaml`,其中每个插件的 `urls`字段是其在文件服务器上的真实访问地址,可以直接下载。 ```bash -python3 make_repo_index.py /root/build_workspace/storages/plugins http://x.x.x.x:8080/plugins /root/build_workspace/storages/plugins +python3 make_repo_index.py /root/build_workspace/storages/plugins http://x.x.x.x:8080/plugins /root/build_workspace/storages/plugins /root/build_workspace/storages/extra.yaml ``` \ No newline at end of file diff --git a/tools/make_repo_index/make_repo_index.py b/tools/make_repo_index/make_repo_index.py index 6e5f806cc74cded4cc588e3e15c82466b0eba983..d4c9c0f42727d5f34eace4d10bc8e645579be0df 100644 --- a/tools/make_repo_index/make_repo_index.py +++ b/tools/make_repo_index/make_repo_index.py @@ -19,6 +19,7 @@ import tarfile import yaml import hashlib import logging +import shutil from datetime import datetime from packaging import version @@ -58,9 +59,15 @@ def validate_and_extract_plugin_info(tar_path): 'name': base_name, # name字段与插件文件名保持一致 'version': main_content.get('version', '1.0.0'), 'updated': main_content.get('updated', ''), + 'author': main_content.get('author', ''), + 'url': main_content.get('url', ''), 'description': main_content.get('description', ''), + 'description_zh': main_content.get('description_zh', ''), + 'description_en': main_content.get('description_en', ''), + 'readme': main_content.get('readme', ''), 'icon': main_content.get('icon', ''), - 'type': main_content.get('type', 'app') + 'type': main_content.get('type', 'app'), + 'localhost_available': main_content.get('localhost_available', False) } return True, info @@ -77,13 +84,149 @@ def calculate_sha256(file_path): sha256_hash.update(byte_block) return sha256_hash.hexdigest() -def generate_index(plugins_dir, url_prefix, output_dir): +def build_plugin_entry(tar_path, plugins_dir, url_prefix, plugin_info): + """构建插件条目字典 + + Args: + tar_path: 插件tar.gz文件路径 + plugins_dir: 插件目录路径 + url_prefix: URL前缀 + plugin_info: 插件信息字典 + + Returns: + dict: 完整的插件条目字典 + """ + # 计算文件大小和sha256 + file_size = os.path.getsize(tar_path) + sha256sum = calculate_sha256(tar_path) + + # 构建相对路径URL,处理可能的双斜杠问题 + rel_path = os.path.relpath(tar_path, plugins_dir) + download_url = os.path.normpath(f"{url_prefix}/{rel_path}").replace('\\', '/') + + # 构建插件条目 + updated_time = datetime.fromtimestamp(os.path.getmtime(tar_path)).astimezone() + formatted_time = updated_time.strftime('%Y-%m-%dT%H:%M:%S%z') + + return { + 'name': plugin_info['name'], + 'version': plugin_info['version'], + 'updated': plugin_info['updated'] or formatted_time, + 'author': plugin_info['author'], + 'url': plugin_info['url'], + 'description': plugin_info['description'], + 'description_zh': plugin_info['description_zh'], + 'description_en': plugin_info['description_en'], + 'readme': plugin_info['readme'], + 'icon': plugin_info['icon'], + 'localhost_available': plugin_info['localhost_available'], + 'type': plugin_info['type'], + 'sha256sum': sha256sum, + 'size': file_size, + 'urls': [download_url] + } + +def load_extra_items(extra_path, plugins_dict): + """加载并合并额外YAML文件中的插件数据 + + Args: + extra_path: 额外YAML文件路径 + plugins_dict: 插件字典 + + Returns: + bool: 是否成功加载 + """ + if not extra_path or not os.path.exists(extra_path): + logger.error(f"Extra item file not found: {extra_path}") + return False + + try: + with open(extra_path, 'r', encoding='utf-8') as f: + extra_data = yaml.safe_load(f) + if not extra_data or 'plugins' not in extra_data: + return True + merge_extra_items(extra_data, plugins_dict) + return True + except Exception as e: + logger.error(f"Error processing extra item file {extra_path}: {e}") + return False + +def merge_extra_items(extra_data, plugins_dict): + for plugin_entry in extra_data['plugins']: + plugin_name, versions = next(iter(plugin_entry.items())) + if plugin_name not in plugins_dict: + plugins_dict[plugin_name] = [] + # 过滤已存在的版本 + existing_versions = {v['version'] for v in plugins_dict[plugin_name]} + new_versions = [v for v in versions if v['version'] not in existing_versions] + plugins_dict[plugin_name].extend(new_versions) + +def extract_plugin_to_details(tar_path, plugin_info, details_dir): + """解压插件到index-details目录并清理workspace目录 + + Args: + tar_path: 插件tar.gz文件路径 + plugin_info: 插件信息字典 + details_dir: index-details目录路径 + """ + try: + # 先解压到临时目录 + temp_dir = os.path.join(details_dir, f"temp_{os.urandom(4).hex()}") + os.makedirs(temp_dir, exist_ok=True) + + # 解压文件 + with tarfile.open(tar_path, 'r:gz') as tar: + tar.extractall(path=temp_dir) + + # 获取解压后的顶层目录 + extracted_dir = os.path.join(temp_dir, os.listdir(temp_dir)[0]) + + # 重命名为目标目录 {name}_{version} + target_dir = os.path.join(details_dir, f"{plugin_info['name']}_{plugin_info['version']}") + os.rename(extracted_dir, target_dir) + + # 删除临时目录 + shutil.rmtree(temp_dir) + + # 删除workspace目录 + workspace_path = os.path.join(target_dir, 'workspace') + if os.path.exists(workspace_path): + shutil.rmtree(workspace_path) + + except Exception as e: + logger.error(f"Error extracting plugin {tar_path} to details dir: {e}") + +def compress_and_clean_details(output_dir): + """压缩index-details目录为tar.gz并删除原目录 + + Args: + output_dir: 输出目录路径 + """ + try: + details_dir = os.path.join(output_dir, 'index-details') + if not os.path.exists(details_dir): + return + + # 压缩目录 + archive_path = os.path.join(output_dir, 'index-details.tar.gz') + with tarfile.open(archive_path, 'w:gz') as tar: + tar.add(details_dir, arcname='index-details') + logger.info(f"Plugin details in {archive_path}") + + # 删除原目录 + shutil.rmtree(details_dir) + + except Exception as e: + logger.error(f"Error compressing details dir: {e}") + +def generate_index(plugins_dir, url_prefix, output_dir, extra_item=''): """生成index.yaml文件 Args: plugins_dir: 插件目录路径 url_prefix: URL前缀 output_dir: 输出目录路径 + extra_item: 额外补充项的yaml文件路径 Returns: bool: 是否成功生成索引文件 @@ -91,6 +234,10 @@ def generate_index(plugins_dir, url_prefix, output_dir): # 使用字典临时存储插件信息 plugins_dict = {} + # 首先处理额外补充项 + if extra_item: + load_extra_items(os.path.abspath(extra_item), plugins_dict) + if not os.path.exists(plugins_dir): logger.error(f"Plugins directory does not exist: {plugins_dir}") return False @@ -99,6 +246,12 @@ def generate_index(plugins_dir, url_prefix, output_dir): logger.error(f"Output directory does not exist: {output_dir}") return False + # 准备index-details目录 + details_dir = os.path.join(output_dir, 'index-details') + if os.path.exists(details_dir): + shutil.rmtree(details_dir) + os.makedirs(details_dir, exist_ok=True) + # 遍历目录查找所有tar.gz文件 for root, _, files in os.walk(plugins_dir): for file in files: @@ -113,29 +266,7 @@ def generate_index(plugins_dir, url_prefix, output_dir): if not valid or plugin_info is None: continue - # 计算文件大小和sha256 - file_size = os.path.getsize(tar_path) - sha256sum = calculate_sha256(tar_path) - - # 构建相对路径URL,处理可能的双斜杠问题 - rel_path = os.path.relpath(tar_path, plugins_dir) - download_url = os.path.normpath(f"{url_prefix}/{rel_path}").replace('\\', '/') - - # 构建插件条目 - updated_time = datetime.fromtimestamp(os.path.getmtime(tar_path)).astimezone() - formatted_time = updated_time.strftime('%Y-%m-%dT%H:%M:%S%z') - - plugin_entry = { - 'name': plugin_info['name'], - 'version': plugin_info['version'], - 'updated': plugin_info['updated'] or formatted_time, - 'description': plugin_info['description'], - 'icon': plugin_info['icon'], - 'type': plugin_info['type'], - 'sha256sum': sha256sum, - 'size': file_size, - 'urls': [download_url] - } + plugin_entry = build_plugin_entry(tar_path, plugins_dir, url_prefix, plugin_info) # 使用插件名作为key plugin_name = plugin_info['name'] @@ -144,6 +275,9 @@ def generate_index(plugins_dir, url_prefix, output_dir): if plugin_name not in plugins_dict: plugins_dict[plugin_name] = [] plugins_dict[plugin_name].append(plugin_entry) + + # 解压插件到index-details目录 + extract_plugin_to_details(tar_path, plugin_info, details_dir) # 准备最终输出数据 index_data = { @@ -153,21 +287,32 @@ def generate_index(plugins_dir, url_prefix, output_dir): # 按插件名排序并处理版本排序 for plugin_name in sorted(plugins_dict.keys()): - # 按版本从大到小排序 - versions = sorted(plugins_dict[plugin_name], - key=lambda x: version.parse(x['version']), - reverse=True) + # 最终去重:确保name+version唯一 + seen_versions = set() + unique_versions = [] + for v in sorted(plugins_dict[plugin_name], + key=lambda x: version.parse(x['version']), + reverse=True): + version_key = (plugin_name, v['version']) + if version_key not in seen_versions: + seen_versions.add(version_key) + unique_versions.append(v) # 添加到最终输出 - index_data['plugins'].append({ - plugin_name: versions - }) + if unique_versions: + index_data['plugins'].append({ + plugin_name: unique_versions + }) # 写入输出文件 output_path = os.path.join(output_dir, 'index.yaml') - with open(output_path, 'w', encoding='utf-8') as f: + fd = os.open(output_path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o644) + with os.fdopen(fd, 'w', encoding='utf-8') as f: yaml.dump(index_data, f, allow_unicode=True, sort_keys=False) + # 压缩并清理index-details目录 + compress_and_clean_details(output_dir) + logger.info(f"Index file generated at: {output_path}") def main(): @@ -175,6 +320,7 @@ def main(): parser.add_argument('plugins_dir', help='Directory containing plugin tar.gz files') parser.add_argument('url_prefix', help='URL prefix for plugin downloads') parser.add_argument('output_dir', help='Directory to output index.yaml') + parser.add_argument('extra_item', default='', help='Path to extra items yaml file (optional)') args = parser.parse_args() @@ -186,7 +332,7 @@ def main(): logger.error(f"Output directory not found: {args.output_dir}") sys.exit(1) - generate_index(args.plugins_dir, args.url_prefix, args.output_dir) + generate_index(args.plugins_dir, args.url_prefix, args.output_dir, args.extra_item) if __name__ == '__main__': main() \ No newline at end of file