diff --git a/.claude/skills/cf-edit/scripts/cf-edit.py b/.claude/skills/cf-edit/scripts/cf-edit.py new file mode 100644 index 00000000..178ae693 --- /dev/null +++ b/.claude/skills/cf-edit/scripts/cf-edit.py @@ -0,0 +1,514 @@ +#!/usr/bin/env python3 +# cf-edit v1.0 — Edit 1C configuration root (Configuration.xml) +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills + +import argparse +import json +import os +import subprocess +import sys +from html import escape as html_escape +from lxml import etree + +MD_NS = "http://v8.1c.ru/8.3/MDClasses" +XR_NS = "http://v8.1c.ru/8.3/xcf/readable" +XSI_NS = "http://www.w3.org/2001/XMLSchema-instance" +V8_NS = "http://v8.1c.ru/8.1/data/core" +XS_NS = "http://www.w3.org/2001/XMLSchema" + +# Canonical type order for ChildObjects (44 types) +TYPE_ORDER = [ + "Language", "Subsystem", "StyleItem", "Style", + "CommonPicture", "SessionParameter", "Role", "CommonTemplate", + "FilterCriterion", "CommonModule", "CommonAttribute", "ExchangePlan", + "XDTOPackage", "WebService", "HTTPService", "WSReference", + "EventSubscription", "ScheduledJob", "SettingsStorage", "FunctionalOption", + "FunctionalOptionsParameter", "DefinedType", "CommonCommand", "CommandGroup", + "Constant", "CommonForm", "Catalog", "Document", + "DocumentNumerator", "Sequence", "DocumentJournal", "Enum", + "Report", "DataProcessor", "InformationRegister", "AccumulationRegister", + "ChartOfCharacteristicTypes", "ChartOfAccounts", "AccountingRegister", + "ChartOfCalculationTypes", "CalculationRegister", + "BusinessProcess", "Task", "IntegrationService", +] + +ML_PROPS = ["Synonym", "BriefInformation", "DetailedInformation", "Copyright", "VendorInformationAddress", "ConfigurationInformationAddress"] +SCALAR_PROPS = ["Name", "Version", "Vendor", "Comment", "NamePrefix", "UpdateCatalogAddress"] +REF_PROPS = ["DefaultLanguage"] + + +def localname(el): + return etree.QName(el.tag).localname + + +def info(msg): + print(f"[INFO] {msg}") + + +def warn(msg): + print(f"[WARN] {msg}") + + +def get_child_indent(container): + if container.text and "\n" in container.text: + after_nl = container.text.rsplit("\n", 1)[-1] + if after_nl and not after_nl.strip(): + return after_nl + for child in container: + if child.tail and "\n" in child.tail: + after_nl = child.tail.rsplit("\n", 1)[-1] + if after_nl and not after_nl.strip(): + return after_nl + depth = 0 + current = container + while current is not None: + depth += 1 + current = current.getparent() + return "\t" * depth + + +def insert_before_closing(container, new_el, child_indent): + children = list(container) + if len(children) == 0: + parent_indent = child_indent[:-1] if len(child_indent) > 0 else "" + container.text = "\r\n" + child_indent + new_el.tail = "\r\n" + parent_indent + container.append(new_el) + else: + last = children[-1] + new_el.tail = last.tail + last.tail = "\r\n" + child_indent + container.append(new_el) + + +def insert_before_ref(container, new_el, ref_el, child_indent): + """Insert new_el before ref_el inside container.""" + idx = list(container).index(ref_el) + prev = ref_el.getprevious() + if prev is not None: + new_el.tail = prev.tail + prev.tail = "\r\n" + child_indent + else: + new_el.tail = container.text + container.text = "\r\n" + child_indent + container.insert(idx, new_el) + + +def remove_with_indent(el): + parent = el.getparent() + prev = el.getprevious() + if prev is not None: + if el.tail: + prev.tail = el.tail + else: + if el.tail: + parent.text = el.tail + parent.remove(el) + + +def expand_self_closing(container, parent_indent): + if len(container) == 0 and not (container.text and container.text.strip()): + container.text = "\r\n" + parent_indent + + +def import_fragment(xml_string): + wrapper = ( + f'<_W xmlns="{MD_NS}" xmlns:xsi="{XSI_NS}" xmlns:v8="{V8_NS}" ' + f'xmlns:xr="{XR_NS}" xmlns:xs="{XS_NS}">{xml_string}' + ) + frag = etree.fromstring(wrapper.encode("utf-8")) + return list(frag) + + +def parse_batch_value(val): + items = [] + for part in val.split(";;"): + trimmed = part.strip() + if trimmed: + items.append(trimmed) + return items + + +def save_xml_bom(tree, path): + xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8") + xml_bytes = xml_bytes.replace(b"encoding='UTF-8'", b'encoding="UTF-8"') + with open(path, "wb") as f: + f.write(b"\xef\xbb\xbf") + f.write(xml_bytes) + + +def main(): + parser = argparse.ArgumentParser(description="Edit 1C configuration root (Configuration.xml)", allow_abbrev=False) + parser.add_argument("-ConfigPath", required=True) + parser.add_argument("-DefinitionFile", default=None) + parser.add_argument("-Operation", default=None, choices=["modify-property", "add-childObject", "remove-childObject", "add-defaultRole", "remove-defaultRole", "set-defaultRoles"]) + parser.add_argument("-Value", default=None) + parser.add_argument("-NoValidate", action="store_true") + args = parser.parse_args() + + if args.DefinitionFile and args.Operation: + print("Cannot use both -DefinitionFile and -Operation", file=sys.stderr) + sys.exit(1) + if not args.DefinitionFile and not args.Operation: + print("Either -DefinitionFile or -Operation is required", file=sys.stderr) + sys.exit(1) + + config_path = args.ConfigPath + if not os.path.isabs(config_path): + config_path = os.path.join(os.getcwd(), config_path) + if os.path.isdir(config_path): + candidate = os.path.join(config_path, "Configuration.xml") + if os.path.isfile(candidate): + config_path = candidate + else: + print("No Configuration.xml in directory", file=sys.stderr) + sys.exit(1) + if not os.path.isfile(config_path): + print(f"File not found: {config_path}", file=sys.stderr) + sys.exit(1) + resolved_path = os.path.abspath(config_path) + + xml_parser = etree.XMLParser(remove_blank_text=False) + tree = etree.parse(resolved_path, xml_parser) + xml_root = tree.getroot() + + add_count = 0 + remove_count = 0 + modify_count = 0 + + cfg_el = None + for child in xml_root: + if isinstance(child.tag, str) and localname(child) == "Configuration": + cfg_el = child + break + if cfg_el is None: + print("No element found", file=sys.stderr) + sys.exit(1) + + props_el = None + child_objs_el = None + for child in cfg_el: + if not isinstance(child.tag, str): + continue + if localname(child) == "Properties": + props_el = child + if localname(child) == "ChildObjects": + child_objs_el = child + + obj_name = "" + if props_el is not None: + for child in props_el: + if isinstance(child.tag, str) and localname(child) == "Name": + obj_name = (child.text or "").strip() + break + info(f"Configuration: {obj_name}") + + # --- Operations --- + def do_modify_property(batch_val): + nonlocal modify_count + items = parse_batch_value(batch_val) + for item in items: + eq_idx = item.find("=") + if eq_idx < 1: + print(f"Invalid property format '{item}', expected 'Key=Value'", file=sys.stderr) + sys.exit(1) + prop_name = item[:eq_idx].strip() + prop_value = item[eq_idx + 1:].strip() + + prop_el = None + for child in props_el: + if isinstance(child.tag, str) and localname(child) == prop_name: + prop_el = child + break + if prop_el is None: + print(f"Property '{prop_name}' not found in Properties", file=sys.stderr) + sys.exit(1) + + if prop_name in ML_PROPS: + for ch in list(prop_el): + prop_el.remove(ch) + if not prop_value: + prop_el.text = None + else: + indent = get_child_indent(props_el) + item_el = etree.SubElement(prop_el, f"{{{V8_NS}}}item") + lang_el = etree.SubElement(item_el, f"{{{V8_NS}}}lang") + lang_el.text = "ru" + content_el = etree.SubElement(item_el, f"{{{V8_NS}}}content") + content_el.text = prop_value + prop_el.text = "\r\n" + indent + "\t" + item_el.text = "\r\n" + indent + "\t\t" + lang_el.tail = "\r\n" + indent + "\t\t" + content_el.tail = "\r\n" + indent + "\t" + item_el.tail = "\r\n" + indent + elif prop_name in SCALAR_PROPS or prop_name in REF_PROPS: + for ch in list(prop_el): + prop_el.remove(ch) + if not prop_value: + prop_el.text = None + else: + prop_el.text = prop_value + else: + for ch in list(prop_el): + prop_el.remove(ch) + prop_el.text = prop_value + + modify_count += 1 + info(f'Set {prop_name} = "{prop_value}"') + + def do_add_child_object(batch_val): + nonlocal add_count + if child_objs_el is None: + print("No element found", file=sys.stderr) + sys.exit(1) + + items = parse_batch_value(batch_val) + cfg_indent = get_child_indent(cfg_el) + if len(child_objs_el) == 0 and not (child_objs_el.text and child_objs_el.text.strip()): + expand_self_closing(child_objs_el, cfg_indent) + child_indent = get_child_indent(child_objs_el) + + for item in items: + dot_idx = item.find(".") + if dot_idx < 1: + print(f"Invalid format '{item}', expected 'Type.Name'", file=sys.stderr) + sys.exit(1) + type_name = item[:dot_idx] + obj_name_val = item[dot_idx + 1:] + + if type_name not in TYPE_ORDER: + print(f"Unknown type '{type_name}'", file=sys.stderr) + sys.exit(1) + type_idx = TYPE_ORDER.index(type_name) + + # Dedup + exists = False + for child in child_objs_el: + if isinstance(child.tag, str) and localname(child) == type_name and (child.text or "") == obj_name_val: + exists = True + break + if exists: + warn(f"Already exists: {type_name}.{obj_name_val}") + continue + + # Find insertion point + insert_before = None + for child in child_objs_el: + if not isinstance(child.tag, str): + continue + child_type_name = localname(child) + if child_type_name not in TYPE_ORDER: + continue + child_type_idx = TYPE_ORDER.index(child_type_name) + + if child_type_name == type_name: + if (child.text or "") > obj_name_val and insert_before is None: + insert_before = child + elif child_type_idx > type_idx and insert_before is None: + insert_before = child + + new_el = etree.Element(f"{{{MD_NS}}}{type_name}") + new_el.text = obj_name_val + + if insert_before is not None: + insert_before_ref(child_objs_el, new_el, insert_before, child_indent) + else: + insert_before_closing(child_objs_el, new_el, child_indent) + + add_count += 1 + info(f"Added: {type_name}.{obj_name_val}") + + def do_remove_child_object(batch_val): + nonlocal remove_count + if child_objs_el is None: + print("No element found", file=sys.stderr) + sys.exit(1) + + items = parse_batch_value(batch_val) + for item in items: + dot_idx = item.find(".") + if dot_idx < 1: + print(f"Invalid format '{item}', expected 'Type.Name'", file=sys.stderr) + sys.exit(1) + type_name = item[:dot_idx] + obj_name_val = item[dot_idx + 1:] + + found = False + for child in list(child_objs_el): + if isinstance(child.tag, str) and localname(child) == type_name and (child.text or "") == obj_name_val: + remove_with_indent(child) + remove_count += 1 + info(f"Removed: {type_name}.{obj_name_val}") + found = True + break + if not found: + warn(f"Not found: {type_name}.{obj_name_val}") + + def do_add_default_role(batch_val): + nonlocal add_count + items = parse_batch_value(batch_val) + + roles_el = None + for child in props_el: + if isinstance(child.tag, str) and localname(child) == "DefaultRoles": + roles_el = child + break + if roles_el is None: + print("No element found in Properties", file=sys.stderr) + sys.exit(1) + + props_indent = get_child_indent(props_el) + if len(roles_el) == 0 and not (roles_el.text and roles_el.text.strip()): + expand_self_closing(roles_el, props_indent) + role_indent = get_child_indent(roles_el) + + for item in items: + role_name = item + if not role_name.startswith("Role."): + role_name = f"Role.{role_name}" + + exists = False + for child in roles_el: + if isinstance(child.tag, str) and (child.text or "").strip() == role_name: + exists = True + break + if exists: + warn(f"DefaultRole already exists: {role_name}") + continue + + frag_xml = f'{role_name}' + nodes = import_fragment(frag_xml) + if nodes: + insert_before_closing(roles_el, nodes[0], role_indent) + add_count += 1 + info(f"Added DefaultRole: {role_name}") + + def do_remove_default_role(batch_val): + nonlocal remove_count + items = parse_batch_value(batch_val) + + roles_el = None + for child in props_el: + if isinstance(child.tag, str) and localname(child) == "DefaultRoles": + roles_el = child + break + if roles_el is None: + print("No element found", file=sys.stderr) + sys.exit(1) + + for item in items: + role_name = item + if not role_name.startswith("Role."): + role_name = f"Role.{role_name}" + + found = False + for child in list(roles_el): + if isinstance(child.tag, str) and (child.text or "").strip() == role_name: + remove_with_indent(child) + remove_count += 1 + info(f"Removed DefaultRole: {role_name}") + found = True + break + if not found: + warn(f"DefaultRole not found: {role_name}") + + def do_set_default_roles(batch_val): + nonlocal modify_count + items = parse_batch_value(batch_val) + + roles_el = None + for child in props_el: + if isinstance(child.tag, str) and localname(child) == "DefaultRoles": + roles_el = child + break + if roles_el is None: + print("No element found", file=sys.stderr) + sys.exit(1) + + # Clear all existing children + for ch in list(roles_el): + roles_el.remove(ch) + roles_el.text = None + + if not items: + modify_count += 1 + info("Cleared DefaultRoles") + return + + props_indent = get_child_indent(props_el) + role_indent = props_indent + "\t" + + roles_el.text = "\r\n" + props_indent + + for item in items: + role_name = item + if not role_name.startswith("Role."): + role_name = f"Role.{role_name}" + + frag_xml = f'{role_name}' + nodes = import_fragment(frag_xml) + if nodes: + insert_before_closing(roles_el, nodes[0], role_indent) + + modify_count += 1 + info(f"Set DefaultRoles: {len(items)} roles") + + # --- Execute operations --- + operations = [] + if args.DefinitionFile: + def_file = args.DefinitionFile + if not os.path.isabs(def_file): + def_file = os.path.join(os.getcwd(), def_file) + with open(def_file, "r", encoding="utf-8-sig") as fh: + ops = json.loads(fh.read()) + if isinstance(ops, list): + operations = ops + else: + operations = [ops] + else: + operations = [{"operation": args.Operation, "value": args.Value or ""}] + + for op in operations: + op_name = op.get("operation", args.Operation or "") + op_value = op.get("value", args.Value or "") + + if op_name == "modify-property": + do_modify_property(op_value) + elif op_name == "add-childObject": + do_add_child_object(op_value) + elif op_name == "remove-childObject": + do_remove_child_object(op_value) + elif op_name == "add-defaultRole": + do_add_default_role(op_value) + elif op_name == "remove-defaultRole": + do_remove_default_role(op_value) + elif op_name == "set-defaultRoles": + do_set_default_roles(op_value) + else: + print(f"Unknown operation: {op_name}", file=sys.stderr) + sys.exit(1) + + # --- Save --- + save_xml_bom(tree, resolved_path) + info(f"Saved: {resolved_path}") + + # --- Auto-validate --- + if not args.NoValidate: + validate_script = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", "..", "cf-validate", "scripts", "cf-validate.py")) + if os.path.isfile(validate_script): + print() + print("--- Running cf-validate ---") + subprocess.run([sys.executable, validate_script, "-ConfigPath", resolved_path]) + + # --- Summary --- + print() + print("=== cf-edit summary ===") + print(f" Configuration: {obj_name}") + print(f" Added: {add_count}") + print(f" Removed: {remove_count}") + print(f" Modified: {modify_count}") + sys.exit(0) + + +if __name__ == "__main__": + main() diff --git a/.claude/skills/cf-info/scripts/cf-info.py b/.claude/skills/cf-info/scripts/cf-info.py new file mode 100644 index 00000000..d8526391 --- /dev/null +++ b/.claude/skills/cf-info/scripts/cf-info.py @@ -0,0 +1,399 @@ +#!/usr/bin/env python3 +# cf-info v1.0 — Compact summary of 1C configuration root +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills + +import argparse +import os +import sys +from collections import OrderedDict +from lxml import etree + +# --- Argument parsing --- +parser = argparse.ArgumentParser(description="Analyze 1C configuration structure", allow_abbrev=False) +parser.add_argument("-ConfigPath", required=True, help="Path to Configuration.xml or directory") +parser.add_argument("-Mode", choices=["overview", "brief", "full"], default="overview", help="Output mode") +parser.add_argument("-Limit", type=int, default=150, help="Max lines to show") +parser.add_argument("-Offset", type=int, default=0, help="Lines to skip") +parser.add_argument("-OutFile", default="", help="Write output to file") +args = parser.parse_args() + +# --- Output helper (collect all, paginate at the end) --- +lines_buf = [] + +def out(text=""): + lines_buf.append(text) + +# --- Resolve path --- +config_path = args.ConfigPath +if not os.path.isabs(config_path): + config_path = os.path.join(os.getcwd(), config_path) + +# Directory -> find Configuration.xml +if os.path.isdir(config_path): + candidate = os.path.join(config_path, "Configuration.xml") + if os.path.isfile(candidate): + config_path = candidate + else: + print(f"[ERROR] No Configuration.xml found in directory: {config_path}", file=sys.stderr) + sys.exit(1) + +if not os.path.isfile(config_path): + print(f"[ERROR] File not found: {config_path}", file=sys.stderr) + sys.exit(1) + +# --- Load XML --- +tree = etree.parse(config_path, etree.XMLParser(remove_blank_text=False)) +xml_root = tree.getroot() +NS = { + "md": "http://v8.1c.ru/8.3/MDClasses", + "v8": "http://v8.1c.ru/8.1/data/core", + "xr": "http://v8.1c.ru/8.3/xcf/readable", + "xsi": "http://www.w3.org/2001/XMLSchema-instance", + "xs": "http://www.w3.org/2001/XMLSchema", + "app": "http://v8.1c.ru/8.2/managed-application/core", +} + +md_root = xml_root # root is MetaDataObject itself +if etree.QName(md_root.tag).localname != "MetaDataObject": + print("[ERROR] Not a valid 1C metadata XML file (no MetaDataObject root)", file=sys.stderr) + sys.exit(1) + +cfg_node = md_root.find("md:Configuration", NS) +if cfg_node is None: + print("[ERROR] No element found", file=sys.stderr) + sys.exit(1) + +version = md_root.get("version", "") +props_node = cfg_node.find("md:Properties", NS) +child_obj_node = cfg_node.find("md:ChildObjects", NS) + +# --- Helpers --- +def get_ml_text(node): + if node is None: + return "" + item = node.find("v8:item/v8:content", NS) + if item is not None and item.text: + return item.text + return "" + +def get_prop_text(prop_name): + n = props_node.find(f"md:{prop_name}", NS) + if n is not None and n.text: + return n.text + return "" + +def get_prop_ml(prop_name): + n = props_node.find(f"md:{prop_name}", NS) + return get_ml_text(n) + +# --- Type name maps (canonical order, 44 types) --- +type_order = [ + "Language", "Subsystem", "StyleItem", "Style", + "CommonPicture", "SessionParameter", "Role", "CommonTemplate", + "FilterCriterion", "CommonModule", "CommonAttribute", "ExchangePlan", + "XDTOPackage", "WebService", "HTTPService", "WSReference", + "EventSubscription", "ScheduledJob", "SettingsStorage", "FunctionalOption", + "FunctionalOptionsParameter", "DefinedType", "CommonCommand", "CommandGroup", + "Constant", "CommonForm", "Catalog", "Document", + "DocumentNumerator", "Sequence", "DocumentJournal", "Enum", + "Report", "DataProcessor", "InformationRegister", "AccumulationRegister", + "ChartOfCharacteristicTypes", "ChartOfAccounts", "AccountingRegister", + "ChartOfCalculationTypes", "CalculationRegister", + "BusinessProcess", "Task", "IntegrationService", +] + +type_ru_names = { + "Language": "Языки", "Subsystem": "Подсистемы", "StyleItem": "Элементы стиля", "Style": "Стили", + "CommonPicture": "Общие картинки", "SessionParameter": "Параметры сеанса", "Role": "Роли", + "CommonTemplate": "Общие макеты", "FilterCriterion": "Критерии отбора", "CommonModule": "Общие модули", + "CommonAttribute": "Общие реквизиты", "ExchangePlan": "Планы обмена", "XDTOPackage": "XDTO-пакеты", + "WebService": "Веб-сервисы", "HTTPService": "HTTP-сервисы", "WSReference": "WS-ссылки", + "EventSubscription": "Подписки на события", "ScheduledJob": "Регламентные задания", + "SettingsStorage": "Хранилища настроек", "FunctionalOption": "Функциональные опции", + "FunctionalOptionsParameter": "Параметры ФО", "DefinedType": "Определяемые типы", + "CommonCommand": "Общие команды", "CommandGroup": "Группы команд", "Constant": "Константы", + "CommonForm": "Общие формы", "Catalog": "Справочники", "Document": "Документы", + "DocumentNumerator": "Нумераторы", "Sequence": "Последовательности", "DocumentJournal": "Журналы документов", + "Enum": "Перечисления", "Report": "Отчёты", "DataProcessor": "Обработки", + "InformationRegister": "Регистры сведений", "AccumulationRegister": "Регистры накопления", + "ChartOfCharacteristicTypes": "ПВХ", "ChartOfAccounts": "Планы счетов", + "AccountingRegister": "Регистры бухгалтерии", "ChartOfCalculationTypes": "ПВР", + "CalculationRegister": "Регистры расчёта", "BusinessProcess": "Бизнес-процессы", + "Task": "Задачи", "IntegrationService": "Сервисы интеграции", +} + +# --- Count objects in ChildObjects --- +object_counts = OrderedDict() +total_objects = 0 + +if child_obj_node is not None: + for child in child_obj_node: + if not isinstance(child.tag, str): + continue # skip comments/PIs + type_name = etree.QName(child.tag).localname + if type_name not in object_counts: + object_counts[type_name] = 0 + object_counts[type_name] += 1 + total_objects += 1 + +# --- Read key properties --- +cfg_name = get_prop_text("Name") +cfg_synonym = get_prop_ml("Synonym") +cfg_version = get_prop_text("Version") +cfg_vendor = get_prop_text("Vendor") +cfg_compat = get_prop_text("CompatibilityMode") +cfg_ext_compat = get_prop_text("ConfigurationExtensionCompatibilityMode") +cfg_default_run = get_prop_text("DefaultRunMode") +cfg_script = get_prop_text("ScriptVariant") +cfg_default_lang = get_prop_text("DefaultLanguage") +cfg_data_lock = get_prop_text("DataLockControlMode") +dash = "\u2014" +cfg_modality = get_prop_text("ModalityUseMode") +cfg_intf_compat = get_prop_text("InterfaceCompatibilityMode") +cfg_auto_num = get_prop_text("ObjectAutonumerationMode") +cfg_sync_calls = get_prop_text("SynchronousPlatformExtensionAndAddInCallUseMode") +cfg_db_spaces = get_prop_text("DatabaseTablespacesUseMode") +cfg_window_mode = get_prop_text("MainClientApplicationWindowMode") + +# --- BRIEF mode --- +if args.Mode == "brief": + syn_part = f' {dash} "{cfg_synonym}"' if cfg_synonym else "" + ver_part = f" v{cfg_version}" if cfg_version else "" + compat_part = f" | {cfg_compat}" if cfg_compat else "" + out(f"Конфигурация: {cfg_name}{syn_part}{ver_part} | {total_objects} объектов{compat_part}") + +# --- OVERVIEW mode --- +if args.Mode == "overview": + syn_part = f' {dash} "{cfg_synonym}"' if cfg_synonym else "" + ver_part = f" v{cfg_version}" if cfg_version else "" + out(f"=== Конфигурация: {cfg_name}{syn_part}{ver_part} ===") + out() + + # Key properties + out(f"Формат: {version}") + if cfg_vendor: + out(f"Поставщик: {cfg_vendor}") + if cfg_version: + out(f"Версия: {cfg_version}") + out(f"Совместимость: {cfg_compat}") + out(f"Режим запуска: {cfg_default_run}") + out(f"Язык скриптов: {cfg_script}") + out(f"Язык: {cfg_default_lang}") + out(f"Блокировки: {cfg_data_lock}") + out(f"Модальность: {cfg_modality}") + out(f"Интерфейс: {cfg_intf_compat}") + out() + + # Object counts table + out(f"--- Состав ({total_objects} объектов) ---") + out() + max_type_len = 0 + for type_name in type_order: + if type_name in object_counts: + ru_name = type_ru_names.get(type_name, type_name) + if len(ru_name) > max_type_len: + max_type_len = len(ru_name) + if max_type_len < 10: + max_type_len = 10 + + for type_name in type_order: + if type_name in object_counts: + count = object_counts[type_name] + ru_name = type_ru_names.get(type_name, type_name) + padded = ru_name.ljust(max_type_len) + out(f" {padded} {count}") + +# --- FULL mode --- +if args.Mode == "full": + syn_part = f' {dash} "{cfg_synonym}"' if cfg_synonym else "" + ver_part = f" v{cfg_version}" if cfg_version else "" + out(f"=== Конфигурация: {cfg_name}{syn_part}{ver_part} ===") + out() + + # --- Section: Identification --- + out("--- Идентификация ---") + out(f"UUID: {cfg_node.get('uuid', '')}") + out(f"Имя: {cfg_name}") + if cfg_synonym: + out(f"Синоним: {cfg_synonym}") + cfg_comment = get_prop_text("Comment") + if cfg_comment: + out(f"Комментарий: {cfg_comment}") + cfg_prefix = get_prop_text("NamePrefix") + if cfg_prefix: + out(f"Префикс: {cfg_prefix}") + if cfg_vendor: + out(f"Поставщик: {cfg_vendor}") + if cfg_version: + out(f"Версия: {cfg_version}") + cfg_update_addr = get_prop_text("UpdateCatalogAddress") + if cfg_update_addr: + out(f"Каталог обн.: {cfg_update_addr}") + out() + + # --- Section: Modes --- + out("--- Режимы работы ---") + out(f"Формат: {version}") + out(f"Совместимость: {cfg_compat}") + out(f"Совм. расширений: {cfg_ext_compat}") + out(f"Режим запуска: {cfg_default_run}") + out(f"Язык скриптов: {cfg_script}") + out(f"Блокировки: {cfg_data_lock}") + out(f"Автонумерация: {cfg_auto_num}") + out(f"Модальность: {cfg_modality}") + out(f"Синхр. вызовы: {cfg_sync_calls}") + out(f"Интерфейс: {cfg_intf_compat}") + out(f"Табл. пространства: {cfg_db_spaces}") + out(f"Режим окна: {cfg_window_mode}") + out() + + # --- Section: Language, roles, purposes --- + out("--- Назначение ---") + out(f"Язык по умолч.: {cfg_default_lang}") + + # UsePurposes + purpose_node = props_node.find("md:UsePurposes", NS) + if purpose_node is not None: + purposes = [] + for val in purpose_node.findall("v8:Value", NS): + if val.text: + purposes.append(val.text) + if purposes: + out(f"Назначения: {', '.join(purposes)}") + + # DefaultRoles + roles_node = props_node.find("md:DefaultRoles", NS) + if roles_node is not None: + roles = [] + for item in roles_node.findall("xr:Item", NS): + if item.text: + roles.append(item.text) + if roles: + out(f"Роли по умолч.: {len(roles)}") + for r in roles: + out(f" - {r}") + + # Booleans + use_mf = get_prop_text("UseManagedFormInOrdinaryApplication") + use_of = get_prop_text("UseOrdinaryFormInManagedApplication") + out(f"Управл.формы в обычн.: {use_mf}") + out(f"Обычн.формы в управл.: {use_of}") + out() + + # --- Section: Storages & default forms --- + out("--- Хранилища и формы по умолчанию ---") + storage_props = [ + "CommonSettingsStorage", "ReportsUserSettingsStorage", "ReportsVariantsStorage", + "FormDataSettingsStorage", "DynamicListsUserSettingsStorage", "URLExternalDataStorage", + ] + for sp in storage_props: + val = get_prop_text(sp) + if val: + out(f" {sp}: {val}") + form_props = [ + "DefaultReportForm", "DefaultReportVariantForm", "DefaultReportSettingsForm", + "DefaultReportAppearanceTemplate", "DefaultDynamicListSettingsForm", "DefaultSearchForm", + "DefaultDataHistoryChangeHistoryForm", "DefaultDataHistoryVersionDataForm", + "DefaultDataHistoryVersionDifferencesForm", "DefaultCollaborationSystemUsersChoiceForm", + "DefaultConstantsForm", "DefaultInterface", "DefaultStyle", + ] + for fp in form_props: + val = get_prop_text(fp) + if val: + out(f" {fp}: {val}") + out() + + # --- Section: Info --- + cfg_brief = get_prop_ml("BriefInformation") + cfg_detail = get_prop_ml("DetailedInformation") + cfg_copyright = get_prop_ml("Copyright") + cfg_vendor_addr = get_prop_ml("VendorInformationAddress") + cfg_info_addr = get_prop_ml("ConfigurationInformationAddress") + if cfg_brief or cfg_detail or cfg_copyright or cfg_vendor_addr or cfg_info_addr: + out("--- Информация ---") + if cfg_brief: + out(f"Краткая: {cfg_brief}") + if cfg_detail: + out(f"Подробная: {cfg_detail}") + if cfg_copyright: + out(f"Copyright: {cfg_copyright}") + if cfg_vendor_addr: + out(f"Сайт поставщика: {cfg_vendor_addr}") + if cfg_info_addr: + out(f"Адрес информ.: {cfg_info_addr}") + out() + + # --- Section: Mobile functionalities --- + mobile_func = props_node.find("md:UsedMobileApplicationFunctionalities", NS) + if mobile_func is not None: + enabled_funcs = [] + disabled_funcs = [] + for func in mobile_func.findall("app:functionality", NS): + f_name = func.find("app:functionality", NS) + f_use = func.find("app:use", NS) + if f_name is not None and f_use is not None: + if f_use.text == "true": + enabled_funcs.append(f_name.text or "") + else: + disabled_funcs.append(f_name.text or "") + total_func = len(enabled_funcs) + len(disabled_funcs) + out(f"--- Мобильные функциональности ({total_func}, включено: {len(enabled_funcs)}) ---") + for f in enabled_funcs: + out(f" [+] {f}") + for f in disabled_funcs: + out(f" [-] {f}") + out() + + # --- Section: InternalInfo --- + internal_info = cfg_node.find("md:InternalInfo", NS) + if internal_info is not None: + contained = internal_info.findall("xr:ContainedObject", NS) + out(f"--- InternalInfo ({len(contained)} ContainedObject) ---") + for co in contained: + class_id_node = co.find("xr:ClassId", NS) + object_id_node = co.find("xr:ObjectId", NS) + class_id = class_id_node.text if class_id_node is not None else "" + object_id = object_id_node.text if object_id_node is not None else "" + out(f" {class_id} -> {object_id}") + out() + + # --- Section: ChildObjects (full list) --- + out(f"--- Состав ({total_objects} объектов) ---") + out() + + for type_name in type_order: + if type_name not in object_counts: + continue + count = object_counts[type_name] + ru_name = type_ru_names.get(type_name, type_name) + out(f" {ru_name} ({type_name}): {count}") + + # Collect names for this type + if child_obj_node is not None: + for child in child_obj_node: + if not isinstance(child.tag, str): + continue + if etree.QName(child.tag).localname == type_name: + out(f" {child.text or ''}") + +# --- Pagination and output --- +total = len(lines_buf) +if args.Offset > 0 or args.Limit < total: + start = min(args.Offset, total) + end = min(start + args.Limit, total) + page = lines_buf[start:end] + result = "\n".join(page) + if end < total: + result += f"\n\n... ({end} of {total} lines, use -Offset {end} to continue)" +else: + result = "\n".join(lines_buf) + +print(result) + +if args.OutFile: + out_file = args.OutFile + if not os.path.isabs(out_file): + out_file = os.path.join(os.getcwd(), out_file) + with open(out_file, "w", encoding="utf-8-sig") as f: + f.write(result) + print(f"\nWritten to: {out_file}") diff --git a/.claude/skills/cf-init/scripts/cf-init.py b/.claude/skills/cf-init/scripts/cf-init.py new file mode 100644 index 00000000..c02471e3 --- /dev/null +++ b/.claude/skills/cf-init/scripts/cf-init.py @@ -0,0 +1,201 @@ +#!/usr/bin/env python3 +# cf-init v1.0 — Create empty 1C configuration scaffold +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +"""Generates minimal XML source files for a 1C configuration.""" +import sys, os, argparse, uuid + +def esc_xml(s): + return s.replace('&','&').replace('<','<').replace('>','>').replace('"','"') + +def new_uuid(): + return str(uuid.uuid4()) + +def write_utf8_bom(path, content): + with open(path, 'w', encoding='utf-8-sig', newline='') as f: + f.write(content) + +def main(): + parser = argparse.ArgumentParser(description='Create empty 1C configuration scaffold', allow_abbrev=False) + parser.add_argument('-Name', dest='Name', required=True) + parser.add_argument('-Synonym', dest='Synonym', default=None) + parser.add_argument('-OutputDir', dest='OutputDir', default='src') + parser.add_argument('-Version', dest='Version', default='') + parser.add_argument('-Vendor', dest='Vendor', default='') + parser.add_argument('-CompatibilityMode', dest='CompatibilityMode', default='Version8_3_24') + args = parser.parse_args() + + name = args.Name + synonym = args.Synonym if args.Synonym else name + output_dir = args.OutputDir + version = args.Version + vendor = args.Vendor + compat = args.CompatibilityMode + + # --- Resolve output dir --- + if not os.path.isabs(output_dir): + output_dir = os.path.join(os.getcwd(), output_dir) + + # --- Check existing --- + cfg_file = os.path.join(output_dir, "Configuration.xml") + if os.path.exists(cfg_file): + print(f"Configuration.xml already exists: {cfg_file}", file=sys.stderr) + sys.exit(1) + + # --- Generate UUIDs --- + uuid_cfg = new_uuid() + uuid_lang = new_uuid() + co = [new_uuid() for _ in range(7)] + + # --- Mobile functionalities --- + mobile_funcs = [ + ("Biometrics","true"), ("Location","false"), ("BackgroundLocation","false"), + ("BluetoothPrinters","false"), ("WiFiPrinters","false"), ("Contacts","false"), + ("Calendars","false"), ("PushNotifications","false"), ("LocalNotifications","false"), + ("InAppPurchases","false"), ("PersonalComputerFileExchange","false"), ("Ads","false"), + ("NumberDialing","false"), ("CallProcessing","false"), ("CallLog","false"), + ("AutoSendSMS","false"), ("ReceiveSMS","false"), ("SMSLog","false"), + ("Camera","false"), ("Microphone","false"), ("MusicLibrary","false"), + ("PictureAndVideoLibraries","false"), ("AudioPlaybackAndVibration","false"), + ("BackgroundAudioPlaybackAndVibration","false"), ("InstallPackages","false"), + ("OSBackup","true"), ("ApplicationUsageStatistics","false"), + ("BarcodeScanning","false"), ("BackgroundAudioRecording","false"), + ("AllFilesAccess","false"), ("Videoconferences","false"), ("NFC","false"), + ("DocumentScanning","false"), ("SpeechToText","false"), ("Geofences","false"), + ("IncomingShareRequests","false"), ("AllIncomingShareRequestsTypesProcessing","false"), + ] + + mobile_xml = "" + for func_name, func_use in mobile_funcs: + mobile_xml += f"\r\n\t\t\t\t\r\n\t\t\t\t\t{func_name}\r\n\t\t\t\t\t{func_use}\r\n\t\t\t\t" + + # --- Synonym XML --- + synonym_xml = "" + if synonym: + synonym_xml = f"\r\n\t\t\t\t\r\n\t\t\t\t\tru\r\n\t\t\t\t\t{esc_xml(synonym)}\r\n\t\t\t\t\r\n\t\t\t" + + vendor_xml = esc_xml(vendor) if vendor else "" + version_xml = esc_xml(version) if version else "" + + class_ids = [ + "9cd510cd-abfc-11d4-9434-004095e12fc7", + "9fcd25a0-4822-11d4-9414-008048da11f9", + "e3687481-0a87-462c-a166-9f34594f9bba", + "9de14907-ec23-4a07-96f0-85521cb6b53b", + "51f2d5d8-ea4d-4064-8892-82951750031e", + "e68182ea-4237-4383-967f-90c1e3370bc7", + "fb282519-d103-4dd3-bc12-cb271d631dfc", + ] + + contained_objects = "" + for i in range(7): + contained_objects += f"""\t\t\t +\t\t\t\t{class_ids[i]} +\t\t\t\t{co[i]} +\t\t\t\n""" + + cfg_xml = f''' + +\t +\t\t +{contained_objects}\t\t +\t\t +\t\t\t{esc_xml(name)} +\t\t\t{synonym_xml} +\t\t\t +\t\t\t +\t\t\t{compat} +\t\t\tManagedApplication +\t\t\t +\t\t\t\tPlatformApplication +\t\t\t +\t\t\tRussian +\t\t\t +\t\t\t{vendor_xml} +\t\t\t{version_xml} +\t\t\t +\t\t\tfalse +\t\t\tfalse +\t\t\tfalse +\t\t\t +\t\t\t +\t\t\t +\t\t\t +\t\t\t +\t\t\t +\t\t\t +\t\t\t +\t\t\t +\t\t\t +\t\t\t +\t\t\t +\t\t\t +\t\t\t +\t\t\t +\t\t\t +\t\t\t +\t\t\t +\t\t\t +\t\t\t{mobile_xml} +\t\t\t +\t\t\t +\t\t\t +\t\t\t +\t\t\tNormal +\t\t\t +\t\t\t +\t\t\tLanguage.Русский +\t\t\t +\t\t\t +\t\t\t +\t\t\t +\t\t\t +\t\t\tManaged +\t\t\tNotAutoFree +\t\t\tDontUse +\t\t\tDontUse +\t\t\tTaxi +\t\t\tDontUse +\t\t\t{compat} +\t\t\t +\t\t +\t\t +\t\t\tРусский +\t\t +\t +''' + + # --- Languages/Русский.xml --- + lang_xml = f''' + +\t +\t\t +\t\t\tРусский +\t\t\t +\t\t\t\t +\t\t\t\t\tru +\t\t\t\t\tРусский +\t\t\t\t +\t\t\t +\t\t\t +\t\t\tru +\t\t +\t +''' + + # --- Create directories --- + os.makedirs(output_dir, exist_ok=True) + lang_dir = os.path.join(output_dir, "Languages") + os.makedirs(lang_dir, exist_ok=True) + + # --- Write files --- + write_utf8_bom(cfg_file, cfg_xml) + lang_file = os.path.join(lang_dir, "Русский.xml") + write_utf8_bom(lang_file, lang_xml) + + print(f"[OK] Создана конфигурация: {name}") + print(f" Каталог: {output_dir}") + print(f" Configuration.xml: {cfg_file}") + print(f" Languages: {lang_file}") + +if __name__ == '__main__': + main() diff --git a/.claude/skills/cf-validate/scripts/cf-validate.py b/.claude/skills/cf-validate/scripts/cf-validate.py new file mode 100644 index 00000000..5779058a --- /dev/null +++ b/.claude/skills/cf-validate/scripts/cf-validate.py @@ -0,0 +1,530 @@ +#!/usr/bin/env python3 +# cf-validate v1.0 — Validate 1C configuration XML structure +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +"""Validates Configuration.xml: root structure, InternalInfo, properties, ChildObjects, languages.""" +import sys, os, argparse, re +from lxml import etree + +NS = { + 'md': 'http://v8.1c.ru/8.3/MDClasses', + 'v8': 'http://v8.1c.ru/8.1/data/core', + 'xr': 'http://v8.1c.ru/8.3/xcf/readable', + 'xsi': 'http://www.w3.org/2001/XMLSchema-instance', + 'xs': 'http://www.w3.org/2001/XMLSchema', + 'app': 'http://v8.1c.ru/8.2/managed-application/core', +} + +GUID_PATTERN = re.compile( + r'^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$' +) +IDENT_PATTERN = re.compile( + r'^[A-Za-z\u0410-\u042F\u0401\u0430-\u044F\u0451_]' + r'[A-Za-z0-9\u0410-\u042F\u0401\u0430-\u044F\u0451_]*$' +) + +# 7 fixed ClassIds for Configuration +VALID_CLASS_IDS = [ + '9cd510cd-abfc-11d4-9434-004095e12fc7', # managed application module + '9fcd25a0-4822-11d4-9414-008048da11f9', # ordinary application module + 'e3687481-0a87-462c-a166-9f34594f9bba', # session module + '9de14907-ec23-4a07-96f0-85521cb6b53b', # external connection module + '51f2d5d8-ea4d-4064-8892-82951750031e', # command interface + 'e68182ea-4237-4383-967f-90c1e3370bc7', # main section command interface + 'fb282519-d103-4dd3-bc12-cb271d631dfc', # home page / client app interface +] + +# 44 types in canonical order +CHILD_OBJECT_TYPES = [ + 'Language', 'Subsystem', 'StyleItem', 'Style', + 'CommonPicture', 'SessionParameter', 'Role', 'CommonTemplate', + 'FilterCriterion', 'CommonModule', 'CommonAttribute', 'ExchangePlan', + 'XDTOPackage', 'WebService', 'HTTPService', 'WSReference', + 'EventSubscription', 'ScheduledJob', 'SettingsStorage', 'FunctionalOption', + 'FunctionalOptionsParameter', 'DefinedType', 'CommonCommand', 'CommandGroup', + 'Constant', 'CommonForm', 'Catalog', 'Document', + 'DocumentNumerator', 'Sequence', 'DocumentJournal', 'Enum', + 'Report', 'DataProcessor', 'InformationRegister', 'AccumulationRegister', + 'ChartOfCharacteristicTypes', 'ChartOfAccounts', 'AccountingRegister', + 'ChartOfCalculationTypes', 'CalculationRegister', + 'BusinessProcess', 'Task', 'IntegrationService', +] + +# Type -> directory mapping +CHILD_TYPE_DIR_MAP = { + 'Language': 'Languages', 'Subsystem': 'Subsystems', 'StyleItem': 'StyleItems', 'Style': 'Styles', + 'CommonPicture': 'CommonPictures', 'SessionParameter': 'SessionParameters', 'Role': 'Roles', + 'CommonTemplate': 'CommonTemplates', 'FilterCriterion': 'FilterCriteria', 'CommonModule': 'CommonModules', + 'CommonAttribute': 'CommonAttributes', 'ExchangePlan': 'ExchangePlans', 'XDTOPackage': 'XDTOPackages', + 'WebService': 'WebServices', 'HTTPService': 'HTTPServices', 'WSReference': 'WSReferences', + 'EventSubscription': 'EventSubscriptions', 'ScheduledJob': 'ScheduledJobs', + 'SettingsStorage': 'SettingsStorages', 'FunctionalOption': 'FunctionalOptions', + 'FunctionalOptionsParameter': 'FunctionalOptionsParameters', 'DefinedType': 'DefinedTypes', + 'CommonCommand': 'CommonCommands', 'CommandGroup': 'CommandGroups', 'Constant': 'Constants', + 'CommonForm': 'CommonForms', 'Catalog': 'Catalogs', 'Document': 'Documents', + 'DocumentNumerator': 'DocumentNumerators', 'Sequence': 'Sequences', + 'DocumentJournal': 'DocumentJournals', 'Enum': 'Enums', 'Report': 'Reports', + 'DataProcessor': 'DataProcessors', 'InformationRegister': 'InformationRegisters', + 'AccumulationRegister': 'AccumulationRegisters', + 'ChartOfCharacteristicTypes': 'ChartsOfCharacteristicTypes', + 'ChartOfAccounts': 'ChartsOfAccounts', 'AccountingRegister': 'AccountingRegisters', + 'ChartOfCalculationTypes': 'ChartsOfCalculationTypes', + 'CalculationRegister': 'CalculationRegisters', + 'BusinessProcess': 'BusinessProcesses', 'Task': 'Tasks', + 'IntegrationService': 'IntegrationServices', +} + +# Valid enum values for Configuration properties +VALID_ENUM_VALUES = { + 'ConfigurationExtensionCompatibilityMode': [ + 'DontUse', 'Version8_1', 'Version8_2_13', 'Version8_2_16', + 'Version8_3_1', 'Version8_3_2', 'Version8_3_3', 'Version8_3_4', 'Version8_3_5', + 'Version8_3_6', 'Version8_3_7', 'Version8_3_8', 'Version8_3_9', 'Version8_3_10', + 'Version8_3_11', 'Version8_3_12', 'Version8_3_13', 'Version8_3_14', 'Version8_3_15', + 'Version8_3_16', 'Version8_3_17', 'Version8_3_18', 'Version8_3_19', 'Version8_3_20', + 'Version8_3_21', 'Version8_3_22', 'Version8_3_23', 'Version8_3_24', 'Version8_3_25', + 'Version8_3_26', 'Version8_3_27', 'Version8_3_28', + ], + 'DefaultRunMode': ['ManagedApplication', 'OrdinaryApplication', 'Auto'], + 'ScriptVariant': ['Russian', 'English'], + 'DataLockControlMode': ['Automatic', 'Managed', 'AutomaticAndManaged'], + 'ObjectAutonumerationMode': ['NotAutoFree', 'AutoFree'], + 'ModalityUseMode': ['DontUse', 'Use', 'UseWithWarnings'], + 'SynchronousPlatformExtensionAndAddInCallUseMode': ['DontUse', 'Use', 'UseWithWarnings'], + 'InterfaceCompatibilityMode': ['Taxi', 'TaxiEnableVersion8_2', 'Version8_2'], + 'DatabaseTablespacesUseMode': ['DontUse', 'Use'], + 'MainClientApplicationWindowMode': ['Normal', 'Fullscreen', 'Kiosk'], + 'CompatibilityMode': [ + 'DontUse', 'Version8_1', 'Version8_2_13', 'Version8_2_16', + 'Version8_3_1', 'Version8_3_2', 'Version8_3_3', 'Version8_3_4', 'Version8_3_5', + 'Version8_3_6', 'Version8_3_7', 'Version8_3_8', 'Version8_3_9', 'Version8_3_10', + 'Version8_3_11', 'Version8_3_12', 'Version8_3_13', 'Version8_3_14', 'Version8_3_15', + 'Version8_3_16', 'Version8_3_17', 'Version8_3_18', 'Version8_3_19', 'Version8_3_20', + 'Version8_3_21', 'Version8_3_22', 'Version8_3_23', 'Version8_3_24', 'Version8_3_25', + 'Version8_3_26', 'Version8_3_27', 'Version8_3_28', + ], +} + +EXPECTED_NS = 'http://v8.1c.ru/8.3/MDClasses' + + +class Reporter: + def __init__(self, max_errors): + self.errors = 0 + self.warnings = 0 + self.stopped = False + self.max_errors = max_errors + self.lines = [] + + def out(self, msg=''): + self.lines.append(msg) + + def ok(self, msg): + self.lines.append(f'[OK] {msg}') + + def error(self, msg): + self.errors += 1 + self.lines.append(f'[ERROR] {msg}') + if self.errors >= self.max_errors: + self.stopped = True + + def warn(self, msg): + self.warnings += 1 + self.lines.append(f'[WARN] {msg}') + + def text(self): + return '\r\n'.join(self.lines) + '\r\n' + + def finalize(self, out_file): + self.out('') + self.out(f'=== Result: {self.errors} errors, {self.warnings} warnings ===') + + result = self.text() + print(result, end='') + + if out_file: + with open(out_file, 'w', encoding='utf-8-sig', newline='') as f: + f.write(result) + print(f'Written to: {out_file}') + + +def main(): + parser = argparse.ArgumentParser( + description='Validate 1C configuration XML structure', allow_abbrev=False + ) + parser.add_argument('-ConfigPath', dest='ConfigPath', required=True) + parser.add_argument('-MaxErrors', dest='MaxErrors', type=int, default=30) + parser.add_argument('-OutFile', dest='OutFile', default='') + args = parser.parse_args() + + config_path = args.ConfigPath + max_errors = args.MaxErrors + out_file = args.OutFile + + # --- Resolve path --- + if not os.path.isabs(config_path): + config_path = os.path.join(os.getcwd(), config_path) + + if os.path.isdir(config_path): + candidate = os.path.join(config_path, 'Configuration.xml') + if os.path.exists(candidate): + config_path = candidate + else: + print(f'[ERROR] No Configuration.xml found in directory: {config_path}') + sys.exit(1) + + if not os.path.exists(config_path): + print(f'[ERROR] File not found: {config_path}') + sys.exit(1) + + resolved_path = os.path.abspath(config_path) + config_dir = os.path.dirname(resolved_path) + + if out_file and not os.path.isabs(out_file): + out_file = os.path.join(os.getcwd(), out_file) + + r = Reporter(max_errors) + r.out('') + + # --- 1. Parse XML --- + xml_doc = None + try: + xml_parser = etree.XMLParser(remove_blank_text=False) + xml_doc = etree.parse(resolved_path, xml_parser) + except etree.XMLSyntaxError as e: + r.lines.insert(0, '=== Validation: Configuration (parse failed) ===') + r.out('') + r.error(f'1. XML parse failed: {e}') + r.finalize(out_file) + sys.exit(1) + + root = xml_doc.getroot() + + # --- Check 1: Root structure --- + check1_ok = True + root_local = etree.QName(root.tag).localname + root_ns = etree.QName(root.tag).namespace or '' + + if root_local != 'MetaDataObject': + r.error(f"1. Root element is '{root_local}', expected 'MetaDataObject'") + r.finalize(out_file) + sys.exit(1) + + if root_ns != EXPECTED_NS: + r.error(f"1. Root namespace is '{root_ns}', expected '{EXPECTED_NS}'") + check1_ok = False + + version = root.get('version', '') + if not version: + r.warn('1. Missing version attribute on MetaDataObject') + elif version not in ('2.17', '2.20'): + r.warn(f"1. Unusual version '{version}' (expected 2.17 or 2.20)") + + # Must have Configuration child + cfg_node = None + for child in root: + if not isinstance(child.tag, str): + continue + if etree.QName(child.tag).localname == 'Configuration' and etree.QName(child.tag).namespace == EXPECTED_NS: + cfg_node = child + break + + if cfg_node is None: + r.error('1. No element found inside MetaDataObject') + r.finalize(out_file) + sys.exit(1) + + # UUID + cfg_uuid = cfg_node.get('uuid', '') + if not cfg_uuid: + r.error('1. Missing uuid on ') + check1_ok = False + elif not GUID_PATTERN.match(cfg_uuid): + r.error(f"1. Invalid uuid '{cfg_uuid}' on ") + check1_ok = False + + # Get name early for header + props_node = cfg_node.find('md:Properties', NS) + name_node = props_node.find('md:Name', NS) if props_node is not None else None + obj_name = (name_node.text or '') if name_node is not None and name_node.text else '(unknown)' + + r.lines.insert(0, f'=== Validation: Configuration.{obj_name} ===') + + if check1_ok: + r.ok(f'1. Root structure: MetaDataObject/Configuration, version {version}') + + if r.stopped: + r.finalize(out_file) + sys.exit(1) + + # --- Check 2: InternalInfo --- + internal_info = cfg_node.find('md:InternalInfo', NS) + check2_ok = True + + if internal_info is None: + r.error('2. InternalInfo: missing') + else: + contained = internal_info.findall('xr:ContainedObject', NS) + if len(contained) != 7: + r.warn(f'2. InternalInfo: expected 7 ContainedObject, found {len(contained)}') + + found_class_ids = {} + for co in contained: + class_id_el = co.find('xr:ClassId', NS) + object_id_el = co.find('xr:ObjectId', NS) + + if class_id_el is None or not (class_id_el.text or ''): + r.error('2. ContainedObject missing ClassId') + check2_ok = False + continue + + cid = class_id_el.text + if cid not in VALID_CLASS_IDS: + r.error(f'2. Unknown ClassId: {cid}') + check2_ok = False + + if cid in found_class_ids: + r.error(f'2. Duplicate ClassId: {cid}') + check2_ok = False + found_class_ids[cid] = True + + if object_id_el is None or not (object_id_el.text or ''): + r.error(f'2. ContainedObject missing ObjectId for ClassId {cid}') + check2_ok = False + elif not GUID_PATTERN.match(object_id_el.text): + r.error(f"2. Invalid ObjectId '{object_id_el.text}' for ClassId {cid}") + check2_ok = False + + # Check missing ClassIds + missing_ids = [cid for cid in VALID_CLASS_IDS if cid not in found_class_ids] + if len(missing_ids) > 0: + r.warn(f'2. Missing ClassIds: {len(missing_ids)} of 7') + + if check2_ok: + r.ok(f'2. InternalInfo: {len(contained)} ContainedObject, all ClassIds valid') + + if r.stopped: + r.finalize(out_file) + sys.exit(1) + + # --- Check 3: Properties -- Name, Synonym, DefaultLanguage, DefaultRunMode --- + def_lang = '' + syn_present = False + + if props_node is None: + r.error('3. Properties block missing') + else: + check3_ok = True + + # Name + if name_node is None or not (name_node.text or ''): + r.error('3. Properties: Name is missing or empty') + check3_ok = False + else: + name_val = name_node.text + if not IDENT_PATTERN.match(name_val): + r.error(f"3. Properties: Name '{name_val}' is not a valid 1C identifier") + check3_ok = False + + # Synonym + syn_node = props_node.find('md:Synonym', NS) + if syn_node is not None: + syn_item = syn_node.find('v8:item', NS) + if syn_item is not None: + syn_content = syn_item.find('v8:content', NS) + if syn_content is not None and syn_content.text: + syn_present = True + + # DefaultLanguage + def_lang_node = props_node.find('md:DefaultLanguage', NS) + def_lang = (def_lang_node.text or '') if def_lang_node is not None else '' + if not def_lang: + r.error('3. Properties: DefaultLanguage is missing or empty') + check3_ok = False + + # DefaultRunMode + def_run_node = props_node.find('md:DefaultRunMode', NS) + if def_run_node is None or not (def_run_node.text or ''): + r.warn('3. Properties: DefaultRunMode is missing or empty') + + if check3_ok: + syn_info = 'Synonym present' if syn_present else 'no Synonym' + r.ok(f'3. Properties: Name="{obj_name}", {syn_info}, DefaultLanguage={def_lang}') + + if r.stopped: + r.finalize(out_file) + sys.exit(1) + + # --- Check 4: Property values -- enum properties --- + if props_node is not None: + enum_checked = 0 + check4_ok = True + + for prop_name, allowed in VALID_ENUM_VALUES.items(): + prop_node = props_node.find(f'md:{prop_name}', NS) + if prop_node is not None and prop_node.text: + val = prop_node.text + if val not in allowed: + r.error(f"4. Property '{prop_name}' has invalid value '{val}'") + check4_ok = False + enum_checked += 1 + + if check4_ok: + r.ok(f'4. Property values: {enum_checked} enum properties checked') + else: + r.warn('4. No Properties block to check') + + if r.stopped: + r.finalize(out_file) + sys.exit(1) + + # --- Check 5: ChildObjects -- valid types, no duplicates, order --- + child_obj_node = cfg_node.find('md:ChildObjects', NS) + + if child_obj_node is None: + r.error('5. ChildObjects block missing') + else: + check5_ok = True + total_count = 0 + type_counts = {} # type_name -> {obj_name: True} + duplicates = {} + type_first_index = {} + last_type_order = -1 + order_ok = True + + for child in child_obj_node: + if not isinstance(child.tag, str): + continue + type_name = etree.QName(child.tag).localname + obj_name_val = child.text or '' + + # Valid type? + if type_name in CHILD_OBJECT_TYPES: + type_idx = CHILD_OBJECT_TYPES.index(type_name) + else: + type_idx = -1 + + if type_idx < 0: + r.error(f"5. Unknown type '{type_name}' in ChildObjects") + check5_ok = False + else: + # Check order + if type_name not in type_first_index: + type_first_index[type_name] = type_idx + if type_idx < last_type_order: + r.warn(f"5. Type '{type_name}' is out of canonical order (after type at position {last_type_order})") + order_ok = False + last_type_order = type_idx + + # Count and dedup + if type_name not in type_counts: + type_counts[type_name] = {} + if obj_name_val in type_counts[type_name]: + dup_key = f'{type_name}.{obj_name_val}' + if dup_key not in duplicates: + r.error(f'5. Duplicate: {dup_key}') + duplicates[dup_key] = True + check5_ok = False + else: + type_counts[type_name][obj_name_val] = True + + total_count += 1 + + type_count = len(type_counts) + if check5_ok: + order_info = ', order correct' if order_ok else '' + r.ok(f'5. ChildObjects: {type_count} types, {total_count} objects{order_info}') + + if r.stopped: + r.finalize(out_file) + sys.exit(1) + + # --- Check 6: DefaultLanguage references existing Language in ChildObjects --- + if def_lang and child_obj_node is not None: + lang_name = def_lang + if lang_name.startswith('Language.'): + lang_name = lang_name[9:] + + found = False + for child in child_obj_node: + if not isinstance(child.tag, str): + continue + if etree.QName(child.tag).localname == 'Language' and (child.text or '') == lang_name: + found = True + break + + if found: + r.ok(f'6. DefaultLanguage "{def_lang}" found in ChildObjects') + else: + r.error(f'6. DefaultLanguage "{def_lang}" not found in ChildObjects') + else: + if not def_lang: + r.warn('6. Cannot check DefaultLanguage (empty)') + else: + r.warn('6. Cannot check DefaultLanguage (no ChildObjects)') + + if r.stopped: + r.finalize(out_file) + sys.exit(1) + + # --- Check 7: Language files exist --- + if child_obj_node is not None: + lang_names = [] + for child in child_obj_node: + if not isinstance(child.tag, str): + continue + if etree.QName(child.tag).localname == 'Language': + lang_names.append(child.text or '') + + if len(lang_names) > 0: + exist_count = 0 + for ln in lang_names: + lang_file = os.path.join(config_dir, 'Languages', ln + '.xml') + if os.path.exists(lang_file): + exist_count += 1 + else: + r.warn(f'7. Language file missing: Languages/{ln}.xml') + if exist_count == len(lang_names): + r.ok(f'7. Language files: {exist_count}/{len(lang_names)} exist') + else: + r.warn('7. No Language entries in ChildObjects') + else: + r.warn('7. Cannot check language files (no ChildObjects)') + + if r.stopped: + r.finalize(out_file) + sys.exit(1) + + # --- Check 8: Object directories exist (spot-check) --- + if child_obj_node is not None: + dirs_to_check = {} + for child in child_obj_node: + if not isinstance(child.tag, str): + continue + type_name = etree.QName(child.tag).localname + if type_name == 'Language': + continue + if type_name in CHILD_TYPE_DIR_MAP: + dir_name = CHILD_TYPE_DIR_MAP[type_name] + dirs_to_check[dir_name] = dirs_to_check.get(dir_name, 0) + 1 + + missing_dirs = [] + for dir_name, count in dirs_to_check.items(): + dir_path = os.path.join(config_dir, dir_name) + if not os.path.isdir(dir_path): + missing_dirs.append(f'{dir_name} ({count} objects)') + + if len(missing_dirs) == 0: + r.ok(f'8. Object directories: {len(dirs_to_check)} directories, all exist') + else: + for md in missing_dirs: + r.warn(f'8. Missing directory: {md}') + else: + r.ok('8. Object directories: N/A') + + # --- Final output --- + r.finalize(out_file) + sys.exit(1 if r.errors > 0 else 0) + + +if __name__ == '__main__': + main() diff --git a/.claude/skills/cfe-borrow/scripts/cfe-borrow.py b/.claude/skills/cfe-borrow/scripts/cfe-borrow.py new file mode 100644 index 00000000..5130f44b --- /dev/null +++ b/.claude/skills/cfe-borrow/scripts/cfe-borrow.py @@ -0,0 +1,844 @@ +#!/usr/bin/env python3 +# cfe-borrow v1.0 — Borrow objects from configuration into extension (CFE) +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills + +import argparse +import os +import re +import sys +import uuid +from lxml import etree + +MD_NS = "http://v8.1c.ru/8.3/MDClasses" +XR_NS = "http://v8.1c.ru/8.3/xcf/readable" +XSI_NS = "http://www.w3.org/2001/XMLSchema-instance" +V8_NS = "http://v8.1c.ru/8.1/data/core" + + +def localname(el): + return etree.QName(el.tag).localname + + +def info(msg): + print(f"[INFO] {msg}") + + +def warn(msg): + print(f"[WARN] {msg}") + + +# --- Type mappings --- +CHILD_TYPE_DIR_MAP = { + "Catalog": "Catalogs", "Document": "Documents", "Enum": "Enums", + "CommonModule": "CommonModules", "CommonPicture": "CommonPictures", + "CommonCommand": "CommonCommands", "CommonTemplate": "CommonTemplates", + "ExchangePlan": "ExchangePlans", "Report": "Reports", "DataProcessor": "DataProcessors", + "InformationRegister": "InformationRegisters", "AccumulationRegister": "AccumulationRegisters", + "ChartOfCharacteristicTypes": "ChartsOfCharacteristicTypes", + "ChartOfAccounts": "ChartsOfAccounts", "AccountingRegister": "AccountingRegisters", + "ChartOfCalculationTypes": "ChartsOfCalculationTypes", "CalculationRegister": "CalculationRegisters", + "BusinessProcess": "BusinessProcesses", "Task": "Tasks", + "Subsystem": "Subsystems", "Role": "Roles", "Constant": "Constants", + "FunctionalOption": "FunctionalOptions", "DefinedType": "DefinedTypes", + "FunctionalOptionsParameter": "FunctionalOptionsParameters", + "CommonForm": "CommonForms", "DocumentJournal": "DocumentJournals", + "SessionParameter": "SessionParameters", "StyleItem": "StyleItems", + "EventSubscription": "EventSubscriptions", "ScheduledJob": "ScheduledJobs", + "SettingsStorage": "SettingsStorages", "FilterCriterion": "FilterCriteria", + "CommandGroup": "CommandGroups", "DocumentNumerator": "DocumentNumerators", + "Sequence": "Sequences", "IntegrationService": "IntegrationServices", + "XDTOPackage": "XDTOPackages", "WebService": "WebServices", + "HTTPService": "HTTPServices", "WSReference": "WSReferences", + "CommonAttribute": "CommonAttributes", "Style": "Styles", +} + +SYNONYM_MAP = { + "\u0421\u043f\u0440\u0430\u0432\u043e\u0447\u043d\u0438\u043a": "Catalog", + "\u0414\u043e\u043a\u0443\u043c\u0435\u043d\u0442": "Document", + "\u041f\u0435\u0440\u0435\u0447\u0438\u0441\u043b\u0435\u043d\u0438\u0435": "Enum", + "\u041e\u0431\u0449\u0438\u0439\u041c\u043e\u0434\u0443\u043b\u044c": "CommonModule", + "\u041e\u0431\u0449\u0430\u044f\u041a\u0430\u0440\u0442\u0438\u043d\u043a\u0430": "CommonPicture", + "\u041e\u0431\u0449\u0430\u044f\u041a\u043e\u043c\u0430\u043d\u0434\u0430": "CommonCommand", + "\u041e\u0431\u0449\u0438\u0439\u041c\u0430\u043a\u0435\u0442": "CommonTemplate", + "\u041f\u043b\u0430\u043d\u041e\u0431\u043c\u0435\u043d\u0430": "ExchangePlan", + "\u041e\u0442\u0447\u0435\u0442": "Report", + "\u041e\u0442\u0447\u0451\u0442": "Report", + "\u041e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430": "DataProcessor", + "\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u0421\u0432\u0435\u0434\u0435\u043d\u0438\u0439": "InformationRegister", + "\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u041d\u0430\u043a\u043e\u043f\u043b\u0435\u043d\u0438\u044f": "AccumulationRegister", + "\u041f\u043b\u0430\u043d\u0412\u0438\u0434\u043e\u0432\u0425\u0430\u0440\u0430\u043a\u0442\u0435\u0440\u0438\u0441\u0442\u0438\u043a": "ChartOfCharacteristicTypes", + "\u041f\u043b\u0430\u043d\u0421\u0447\u0435\u0442\u043e\u0432": "ChartOfAccounts", + "\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u0411\u0443\u0445\u0433\u0430\u043b\u0442\u0435\u0440\u0438\u0438": "AccountingRegister", + "\u041f\u043b\u0430\u043d\u0412\u0438\u0434\u043e\u0432\u0420\u0430\u0441\u0447\u0435\u0442\u0430": "ChartOfCalculationTypes", + "\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u0420\u0430\u0441\u0447\u0435\u0442\u0430": "CalculationRegister", + "\u0411\u0438\u0437\u043d\u0435\u0441\u041f\u0440\u043e\u0446\u0435\u0441\u0441": "BusinessProcess", + "\u0417\u0430\u0434\u0430\u0447\u0430": "Task", + "\u041f\u043e\u0434\u0441\u0438\u0441\u0442\u0435\u043c\u0430": "Subsystem", + "\u0420\u043e\u043b\u044c": "Role", + "\u041a\u043e\u043d\u0441\u0442\u0430\u043d\u0442\u0430": "Constant", + "\u0424\u0443\u043d\u043a\u0446\u0438\u043e\u043d\u0430\u043b\u044c\u043d\u0430\u044f\u041e\u043f\u0446\u0438\u044f": "FunctionalOption", + "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u043c\u044b\u0439\u0422\u0438\u043f": "DefinedType", + "\u041e\u0431\u0449\u0430\u044f\u0424\u043e\u0440\u043c\u0430": "CommonForm", + "\u0416\u0443\u0440\u043d\u0430\u043b\u0414\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u043e\u0432": "DocumentJournal", + "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0421\u0435\u0430\u043d\u0441\u0430": "SessionParameter", + "\u0413\u0440\u0443\u043f\u043f\u0430\u041a\u043e\u043c\u0430\u043d\u0434": "CommandGroup", + "\u041f\u043e\u0434\u043f\u0438\u0441\u043a\u0430\u041d\u0430\u0421\u043e\u0431\u044b\u0442\u0438\u0435": "EventSubscription", + "\u0420\u0435\u0433\u043b\u0430\u043c\u0435\u043d\u0442\u043d\u043e\u0435\u0417\u0430\u0434\u0430\u043d\u0438\u0435": "ScheduledJob", + "\u041e\u0431\u0449\u0438\u0439\u0420\u0435\u043a\u0432\u0438\u0437\u0438\u0442": "CommonAttribute", + "\u041f\u0430\u043a\u0435\u0442XDTO": "XDTOPackage", + "HTTP\u0421\u0435\u0440\u0432\u0438\u0441": "HTTPService", + "\u0421\u0435\u0440\u0432\u0438\u0441\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438": "IntegrationService", +} + +TYPE_ORDER = [ + "Language", "Subsystem", "StyleItem", "Style", + "CommonPicture", "SessionParameter", "Role", "CommonTemplate", + "FilterCriterion", "CommonModule", "CommonAttribute", "ExchangePlan", + "XDTOPackage", "WebService", "HTTPService", "WSReference", + "EventSubscription", "ScheduledJob", "SettingsStorage", "FunctionalOption", + "FunctionalOptionsParameter", "DefinedType", "CommonCommand", "CommandGroup", + "Constant", "CommonForm", "Catalog", "Document", + "DocumentNumerator", "Sequence", "DocumentJournal", "Enum", + "Report", "DataProcessor", "InformationRegister", "AccumulationRegister", + "ChartOfCharacteristicTypes", "ChartOfAccounts", "AccountingRegister", + "ChartOfCalculationTypes", "CalculationRegister", + "BusinessProcess", "Task", "IntegrationService", +] + +GENERATED_TYPES = { + "Catalog": [ + {"prefix": "CatalogObject", "category": "Object"}, + {"prefix": "CatalogRef", "category": "Ref"}, + {"prefix": "CatalogSelection", "category": "Selection"}, + {"prefix": "CatalogList", "category": "List"}, + {"prefix": "CatalogManager", "category": "Manager"}, + ], + "Document": [ + {"prefix": "DocumentObject", "category": "Object"}, + {"prefix": "DocumentRef", "category": "Ref"}, + {"prefix": "DocumentSelection", "category": "Selection"}, + {"prefix": "DocumentList", "category": "List"}, + {"prefix": "DocumentManager", "category": "Manager"}, + ], + "Enum": [ + {"prefix": "EnumRef", "category": "Ref"}, + {"prefix": "EnumManager", "category": "Manager"}, + {"prefix": "EnumList", "category": "List"}, + ], + "Constant": [ + {"prefix": "ConstantManager", "category": "Manager"}, + {"prefix": "ConstantValueManager", "category": "ValueManager"}, + {"prefix": "ConstantValueKey", "category": "ValueKey"}, + ], + "InformationRegister": [ + {"prefix": "InformationRegisterRecord", "category": "Record"}, + {"prefix": "InformationRegisterManager", "category": "Manager"}, + {"prefix": "InformationRegisterSelection", "category": "Selection"}, + {"prefix": "InformationRegisterList", "category": "List"}, + {"prefix": "InformationRegisterRecordSet", "category": "RecordSet"}, + {"prefix": "InformationRegisterRecordKey", "category": "RecordKey"}, + {"prefix": "InformationRegisterRecordManager", "category": "RecordManager"}, + ], + "AccumulationRegister": [ + {"prefix": "AccumulationRegisterRecord", "category": "Record"}, + {"prefix": "AccumulationRegisterManager", "category": "Manager"}, + {"prefix": "AccumulationRegisterSelection", "category": "Selection"}, + {"prefix": "AccumulationRegisterList", "category": "List"}, + {"prefix": "AccumulationRegisterRecordSet", "category": "RecordSet"}, + {"prefix": "AccumulationRegisterRecordKey", "category": "RecordKey"}, + ], + "AccountingRegister": [ + {"prefix": "AccountingRegisterRecord", "category": "Record"}, + {"prefix": "AccountingRegisterManager", "category": "Manager"}, + {"prefix": "AccountingRegisterSelection", "category": "Selection"}, + {"prefix": "AccountingRegisterList", "category": "List"}, + {"prefix": "AccountingRegisterRecordSet", "category": "RecordSet"}, + {"prefix": "AccountingRegisterRecordKey", "category": "RecordKey"}, + ], + "CalculationRegister": [ + {"prefix": "CalculationRegisterRecord", "category": "Record"}, + {"prefix": "CalculationRegisterManager", "category": "Manager"}, + {"prefix": "CalculationRegisterSelection", "category": "Selection"}, + {"prefix": "CalculationRegisterList", "category": "List"}, + {"prefix": "CalculationRegisterRecordSet", "category": "RecordSet"}, + {"prefix": "CalculationRegisterRecordKey", "category": "RecordKey"}, + ], + "ChartOfAccounts": [ + {"prefix": "ChartOfAccountsObject", "category": "Object"}, + {"prefix": "ChartOfAccountsRef", "category": "Ref"}, + {"prefix": "ChartOfAccountsSelection", "category": "Selection"}, + {"prefix": "ChartOfAccountsList", "category": "List"}, + {"prefix": "ChartOfAccountsManager", "category": "Manager"}, + ], + "ChartOfCharacteristicTypes": [ + {"prefix": "ChartOfCharacteristicTypesObject", "category": "Object"}, + {"prefix": "ChartOfCharacteristicTypesRef", "category": "Ref"}, + {"prefix": "ChartOfCharacteristicTypesSelection", "category": "Selection"}, + {"prefix": "ChartOfCharacteristicTypesList", "category": "List"}, + {"prefix": "ChartOfCharacteristicTypesManager", "category": "Manager"}, + ], + "ChartOfCalculationTypes": [ + {"prefix": "ChartOfCalculationTypesObject", "category": "Object"}, + {"prefix": "ChartOfCalculationTypesRef", "category": "Ref"}, + {"prefix": "ChartOfCalculationTypesSelection", "category": "Selection"}, + {"prefix": "ChartOfCalculationTypesList", "category": "List"}, + {"prefix": "ChartOfCalculationTypesManager", "category": "Manager"}, + {"prefix": "DisplacingCalculationTypes", "category": "DisplacingCalculationTypes"}, + {"prefix": "BaseCalculationTypes", "category": "BaseCalculationTypes"}, + {"prefix": "LeadingCalculationTypes", "category": "LeadingCalculationTypes"}, + ], + "BusinessProcess": [ + {"prefix": "BusinessProcessObject", "category": "Object"}, + {"prefix": "BusinessProcessRef", "category": "Ref"}, + {"prefix": "BusinessProcessSelection", "category": "Selection"}, + {"prefix": "BusinessProcessList", "category": "List"}, + {"prefix": "BusinessProcessManager", "category": "Manager"}, + ], + "Task": [ + {"prefix": "TaskObject", "category": "Object"}, + {"prefix": "TaskRef", "category": "Ref"}, + {"prefix": "TaskSelection", "category": "Selection"}, + {"prefix": "TaskList", "category": "List"}, + {"prefix": "TaskManager", "category": "Manager"}, + ], + "ExchangePlan": [ + {"prefix": "ExchangePlanObject", "category": "Object"}, + {"prefix": "ExchangePlanRef", "category": "Ref"}, + {"prefix": "ExchangePlanSelection", "category": "Selection"}, + {"prefix": "ExchangePlanList", "category": "List"}, + {"prefix": "ExchangePlanManager", "category": "Manager"}, + ], + "DocumentJournal": [ + {"prefix": "DocumentJournalSelection", "category": "Selection"}, + {"prefix": "DocumentJournalList", "category": "List"}, + {"prefix": "DocumentJournalManager", "category": "Manager"}, + ], + "Report": [ + {"prefix": "ReportObject", "category": "Object"}, + {"prefix": "ReportManager", "category": "Manager"}, + ], + "DataProcessor": [ + {"prefix": "DataProcessorObject", "category": "Object"}, + {"prefix": "DataProcessorManager", "category": "Manager"}, + ], +} + +TYPES_WITH_CHILD_OBJECTS = [ + "Catalog", "Document", "ExchangePlan", "ChartOfAccounts", + "ChartOfCharacteristicTypes", "ChartOfCalculationTypes", + "BusinessProcess", "Task", "Enum", + "InformationRegister", "AccumulationRegister", "AccountingRegister", "CalculationRegister", +] + +COMMON_MODULE_PROPS = ["Global", "ClientManagedApplication", "Server", "ExternalConnection", "ClientOrdinaryApplication", "ServerCall"] + +XMLNS_DECL = ( + 'xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" ' + 'xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" ' + 'xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" ' + 'xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" ' + 'xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" ' + 'xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" ' + 'xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" ' + 'xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" ' + 'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"' +) + + +def get_child_indent(container): + if container.text and "\n" in container.text: + after_nl = container.text.rsplit("\n", 1)[-1] + if after_nl and not after_nl.strip(): + return after_nl + for child in container: + if child.tail and "\n" in child.tail: + after_nl = child.tail.rsplit("\n", 1)[-1] + if after_nl and not after_nl.strip(): + return after_nl + depth = 0 + current = container + while current is not None: + depth += 1 + current = current.getparent() + return "\t" * depth + + +def insert_before_closing(container, new_el, child_indent): + children = list(container) + if len(children) == 0: + parent_indent = child_indent[:-1] if len(child_indent) > 0 else "" + container.text = "\r\n" + child_indent + new_el.tail = "\r\n" + parent_indent + container.append(new_el) + else: + last = children[-1] + new_el.tail = last.tail + last.tail = "\r\n" + child_indent + container.append(new_el) + + +def insert_before_ref(container, new_el, ref_el, child_indent): + idx = list(container).index(ref_el) + prev = ref_el.getprevious() + if prev is not None: + new_el.tail = prev.tail + prev.tail = "\r\n" + child_indent + else: + new_el.tail = container.text + container.text = "\r\n" + child_indent + container.insert(idx, new_el) + + +def expand_self_closing(container, parent_indent): + if len(container) == 0 and not (container.text and container.text.strip()): + container.text = "\r\n" + parent_indent + + +def save_xml_bom(tree, path): + xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8") + xml_bytes = xml_bytes.replace(b"encoding='UTF-8'", b'encoding="UTF-8"') + with open(path, "wb") as f: + f.write(b"\xef\xbb\xbf") + f.write(xml_bytes) + + +def save_text_bom(path, text): + with open(path, "w", encoding="utf-8-sig") as fh: + fh.write(text) + + +def new_guid(): + return str(uuid.uuid4()) + + +def main(): + parser = argparse.ArgumentParser(description="Borrow objects from configuration into extension", allow_abbrev=False) + parser.add_argument("-ExtensionPath", required=True) + parser.add_argument("-ConfigPath", required=True) + parser.add_argument("-Object", required=True) + args = parser.parse_args() + + # --- 1. Resolve paths --- + ext_path = args.ExtensionPath + if not os.path.isabs(ext_path): + ext_path = os.path.join(os.getcwd(), ext_path) + if os.path.isdir(ext_path): + candidate = os.path.join(ext_path, "Configuration.xml") + if os.path.isfile(candidate): + ext_path = candidate + else: + print(f"No Configuration.xml in extension directory: {ext_path}", file=sys.stderr) + sys.exit(1) + if not os.path.isfile(ext_path): + print(f"Extension file not found: {ext_path}", file=sys.stderr) + sys.exit(1) + ext_resolved = os.path.abspath(ext_path) + ext_dir = os.path.dirname(ext_resolved) + + cfg_path = args.ConfigPath + if not os.path.isabs(cfg_path): + cfg_path = os.path.join(os.getcwd(), cfg_path) + if os.path.isdir(cfg_path): + candidate = os.path.join(cfg_path, "Configuration.xml") + if os.path.isfile(candidate): + cfg_path = candidate + else: + print(f"No Configuration.xml in config directory: {cfg_path}", file=sys.stderr) + sys.exit(1) + if not os.path.isfile(cfg_path): + print(f"Config file not found: {cfg_path}", file=sys.stderr) + sys.exit(1) + cfg_resolved = os.path.abspath(cfg_path) + cfg_dir = os.path.dirname(cfg_resolved) + + # --- 2. Load extension Configuration.xml --- + xml_parser = etree.XMLParser(remove_blank_text=False) + tree = etree.parse(ext_resolved, xml_parser) + xml_root = tree.getroot() + + cfg_el = None + for child in xml_root: + if isinstance(child.tag, str) and localname(child) == "Configuration": + cfg_el = child + break + if cfg_el is None: + print("No element found in extension", file=sys.stderr) + sys.exit(1) + + props_el = None + child_objs_el = None + for child in cfg_el: + if not isinstance(child.tag, str): + continue + if localname(child) == "Properties": + props_el = child + if localname(child) == "ChildObjects": + child_objs_el = child + + if props_el is None: + print("No element found in extension", file=sys.stderr) + sys.exit(1) + if child_objs_el is None: + print("No element found in extension", file=sys.stderr) + sys.exit(1) + + # --- 3. Extract NamePrefix --- + name_prefix = "" + for child in props_el: + if isinstance(child.tag, str) and localname(child) == "NamePrefix": + name_prefix = (child.text or "").strip() + break + info(f"Extension NamePrefix: {name_prefix}") + + # --- Helper functions --- + def read_source_object(type_name, obj_name): + dir_name = CHILD_TYPE_DIR_MAP.get(type_name) + if not dir_name: + print(f"Unknown type '{type_name}'", file=sys.stderr) + sys.exit(1) + + src_file = os.path.join(cfg_dir, dir_name, f"{obj_name}.xml") + if not os.path.isfile(src_file): + print(f"Source object not found: {src_file}", file=sys.stderr) + sys.exit(1) + + src_parser = etree.XMLParser(remove_blank_text=True) + src_tree = etree.parse(src_file, src_parser) + src_root = src_tree.getroot() + + src_el = None + for c in src_root: + if isinstance(c.tag, str): + src_el = c + break + if src_el is None: + print(f"No metadata element found in {dir_name}/{obj_name}.xml", file=sys.stderr) + sys.exit(1) + + src_uuid = src_el.get("uuid", "") + if not src_uuid: + print(f"No uuid attribute on source element in {dir_name}/{obj_name}.xml", file=sys.stderr) + sys.exit(1) + + src_props = {} + props_node = src_el.find(f"{{{MD_NS}}}Properties") + if props_node is not None: + for prop_name in COMMON_MODULE_PROPS: + prop_node = props_node.find(f"{{{MD_NS}}}{prop_name}") + if prop_node is not None: + src_props[prop_name] = (prop_node.text or "").strip() + + return {"Uuid": src_uuid, "Properties": src_props, "Element": src_el} + + def read_source_form_uuid(type_name, obj_name, form_name): + dir_name = CHILD_TYPE_DIR_MAP[type_name] + src_file = os.path.join(cfg_dir, dir_name, obj_name, "Forms", f"{form_name}.xml") + if not os.path.isfile(src_file): + print(f"Source form not found: {src_file}", file=sys.stderr) + sys.exit(1) + + src_parser = etree.XMLParser(remove_blank_text=True) + src_tree = etree.parse(src_file, src_parser) + + src_el = None + for c in src_tree.getroot(): + if isinstance(c.tag, str): + src_el = c + break + if src_el is None: + print(f"No metadata element found in source form: {src_file}", file=sys.stderr) + sys.exit(1) + + src_uuid = src_el.get("uuid", "") + if not src_uuid: + print(f"No uuid attribute on source form element: {src_file}", file=sys.stderr) + sys.exit(1) + return src_uuid + + def build_internal_info_xml(type_name, obj_name, indent): + types = GENERATED_TYPES.get(type_name) + if not types: + return f"{indent}" + + lines = [f"{indent}"] + + if type_name == "ExchangePlan": + this_node_uuid = new_guid() + lines.append(f"{indent}\t{this_node_uuid}") + + for gt in types: + full_name = f"{gt['prefix']}.{obj_name}" + type_id = new_guid() + value_id = new_guid() + lines.append(f'{indent}\t') + lines.append(f"{indent}\t\t{type_id}") + lines.append(f"{indent}\t\t{value_id}") + lines.append(f"{indent}\t") + + lines.append(f"{indent}") + return "\n".join(lines) + + def build_borrowed_object_xml(type_name, obj_name, source_uuid, source_props): + new_uuid_val = new_guid() + internal_info_xml = build_internal_info_xml(type_name, obj_name, "\t\t") + + lines = [] + lines.append('') + lines.append(f'') + lines.append(f'\t<{type_name} uuid="{new_uuid_val}">') + lines.append(internal_info_xml) + lines.append("\t\t") + lines.append("\t\t\tAdopted") + lines.append(f"\t\t\t{obj_name}") + lines.append("\t\t\t") + lines.append(f"\t\t\t{source_uuid}") + + if type_name == "CommonModule": + for prop_name in COMMON_MODULE_PROPS: + prop_val = source_props.get(prop_name, "false") + lines.append(f"\t\t\t<{prop_name}>{prop_val}") + + lines.append("\t\t") + + if type_name in TYPES_WITH_CHILD_OBJECTS: + lines.append("\t\t") + + lines.append(f"\t") + lines.append("") + return "\n".join(lines) + + def add_to_child_objects(type_name, obj_name): + cfg_indent = get_child_indent(cfg_el) + if len(child_objs_el) == 0 and not (child_objs_el.text and child_objs_el.text.strip()): + expand_self_closing(child_objs_el, cfg_indent) + ci = get_child_indent(child_objs_el) + + if type_name not in TYPE_ORDER: + print(f"Unknown type '{type_name}' for ChildObjects ordering", file=sys.stderr) + sys.exit(1) + type_idx = TYPE_ORDER.index(type_name) + + # Dedup + for child in child_objs_el: + if isinstance(child.tag, str) and localname(child) == type_name and (child.text or "") == obj_name: + warn(f"Already in ChildObjects: {type_name}.{obj_name}") + return + + insert_before = None + for child in child_objs_el: + if not isinstance(child.tag, str): + continue + child_type_name = localname(child) + if child_type_name not in TYPE_ORDER: + continue + child_type_idx = TYPE_ORDER.index(child_type_name) + + if child_type_name == type_name: + if (child.text or "") > obj_name and insert_before is None: + insert_before = child + elif child_type_idx > type_idx and insert_before is None: + insert_before = child + + new_el = etree.Element(f"{{{MD_NS}}}{type_name}") + new_el.text = obj_name + + if insert_before is not None: + insert_before_ref(child_objs_el, new_el, insert_before, ci) + else: + insert_before_closing(child_objs_el, new_el, ci) + + info(f"Added to ChildObjects: {type_name}.{obj_name}") + + def test_object_borrowed(type_name, obj_name): + dir_name = CHILD_TYPE_DIR_MAP[type_name] + obj_file = os.path.join(ext_dir, dir_name, f"{obj_name}.xml") + return os.path.isfile(obj_file) + + def register_form_in_object(type_name, obj_name, form_name): + dir_name = CHILD_TYPE_DIR_MAP[type_name] + obj_file = os.path.join(ext_dir, dir_name, f"{obj_name}.xml") + if not os.path.isfile(obj_file): + warn(f"Parent object file not found: {obj_file} \u2014 form not registered in ChildObjects") + return + + obj_parser = etree.XMLParser(remove_blank_text=False) + obj_tree = etree.parse(obj_file, obj_parser) + obj_root = obj_tree.getroot() + + obj_el = None + for c in obj_root: + if isinstance(c.tag, str): + obj_el = c + break + if obj_el is None: + warn(f"No type element in {obj_file} \u2014 form not registered") + return + + child_objs = obj_el.find(f"{{{MD_NS}}}ChildObjects") + if child_objs is None: + child_objs = etree.SubElement(obj_el, f"{{{MD_NS}}}ChildObjects") + # Set proper whitespace + prev = child_objs.getprevious() + if prev is not None: + child_objs.tail = "\r\n\t" + prev_tail = prev.tail or "" + if not prev_tail.endswith("\t\t"): + prev.tail = "\r\n\t\t" + + # Dedup + for c in child_objs: + if isinstance(c.tag, str) and localname(c) == "Form" and (c.text or "") == form_name: + warn(f"Form '{form_name}' already in ChildObjects of {type_name}.{obj_name}") + return + + if len(child_objs) == 0 and not (child_objs.text and child_objs.text.strip()): + child_objs.text = "\r\n\t\t" + + form_el = etree.Element(f"{{{MD_NS}}}Form") + form_el.text = form_name + insert_before_closing(child_objs, form_el, "\t\t\t") + + save_xml_bom(obj_tree, obj_file) + info(f" Registered form in: {obj_file}") + + def borrow_form(type_name, obj_name, form_name): + dir_name = CHILD_TYPE_DIR_MAP[type_name] + + # 1. Read source form UUID + form_uuid = read_source_form_uuid(type_name, obj_name, form_name) + info(f" Source form UUID: {form_uuid}") + + # 2. Read source Form.xml + src_form_xml_path = os.path.join(cfg_dir, dir_name, obj_name, "Forms", form_name, "Ext", "Form.xml") + if not os.path.isfile(src_form_xml_path): + print(f"Source Form.xml not found: {src_form_xml_path}", file=sys.stderr) + sys.exit(1) + with open(src_form_xml_path, "r", encoding="utf-8-sig") as fh: + src_form_content = fh.read() + + # 3. Generate form metadata XML + new_form_uuid = new_guid() + form_meta_lines = [ + '', + f'', + f'\t
', + '\t\t', + '\t\t', + '\t\t\tAdopted', + f'\t\t\t{form_name}', + '\t\t\t', + f'\t\t\t{form_uuid}', + '\t\t\tManaged', + '\t\t', + '\t', + '
', + ] + + # 4. Create directories + form_meta_dir = os.path.join(ext_dir, dir_name, obj_name, "Forms") + os.makedirs(form_meta_dir, exist_ok=True) + + form_meta_file = os.path.join(form_meta_dir, f"{form_name}.xml") + save_text_bom(form_meta_file, "\n".join(form_meta_lines)) + info(f" Created: {form_meta_file}") + + # 5. Generate Form.xml with BaseForm + src_form_parser = etree.XMLParser(remove_blank_text=False) + src_form_tree = etree.parse(src_form_xml_path, src_form_parser) + src_form_el = src_form_tree.getroot() + + form_version = src_form_el.get("version", "2.17") + + src_auto_cmd = None + src_child_items = None + for fc in src_form_el: + if not isinstance(fc.tag, str): + continue + ln = localname(fc) + if ln == "AutoCommandBar" and src_auto_cmd is None: + src_auto_cmd = fc + elif ln == "ChildItems" and src_child_items is None: + src_child_items = fc + + ns_strip_pattern = re.compile(r'\s+xmlns(?::\w+)?="[^"]*"') + + auto_cmd_xml = "" + if src_auto_cmd is not None: + auto_cmd_xml = etree.tostring(src_auto_cmd, encoding="unicode") + auto_cmd_xml = ns_strip_pattern.sub("", auto_cmd_xml) + auto_cmd_xml = re.sub(r'[^<]*', '0', auto_cmd_xml) + auto_cmd_xml = auto_cmd_xml.replace('true', 'false') + + child_items_xml = "" + if src_child_items is not None: + child_items_xml = etree.tostring(src_child_items, encoding="unicode") + child_items_xml = ns_strip_pattern.sub("", child_items_xml) + child_items_xml = re.sub(r'[^<]*', '0', child_items_xml) + else: + child_items_xml = "" + + # Extract source form opening tag + xml_decl = '' + form_tag = f'
' + m_decl = re.search(r'^(<\?xml[^?]*\?>)', src_form_content) + if m_decl: + xml_decl = m_decl.group(1) + m_tag = re.search(r'(]*>)', src_form_content) + if m_tag: + form_tag = m_tag.group(1) + + # Build output + parts = [] + parts.append(xml_decl) + parts.append("\r\n") + parts.append(form_tag) + parts.append("\r\n") + + if auto_cmd_xml: + parts.append(f"\t{auto_cmd_xml}\r\n") + parts.append(f"\t{child_items_xml}\r\n") + parts.append("\t\r\n") + + # BaseForm + parts.append(f'\t\r\n') + + if auto_cmd_xml: + ac_lines = auto_cmd_xml.split("\n") + for li, line in enumerate(ac_lines): + if li == 0: + parts.append(f"\t\t{line}") + else: + parts.append(f"\t{line}") + parts.append("\r\n") + + ci_lines = child_items_xml.split("\n") + for li, line in enumerate(ci_lines): + if li == 0: + parts.append(f"\t\t{line}") + else: + parts.append(f"\t{line}") + parts.append("\r\n") + + parts.append("\t\t\r\n") + parts.append("\t\r\n") + parts.append("") + + form_xml_dir = os.path.join(form_meta_dir, form_name, "Ext") + os.makedirs(form_xml_dir, exist_ok=True) + form_xml_file = os.path.join(form_xml_dir, "Form.xml") + save_text_bom(form_xml_file, "".join(parts)) + info(f" Created: {form_xml_file}") + + # 6. Create empty Module.bsl + module_dir = os.path.join(form_xml_dir, "Form") + os.makedirs(module_dir, exist_ok=True) + module_bsl_file = os.path.join(module_dir, "Module.bsl") + save_text_bom(module_bsl_file, "") + info(f" Created: {module_bsl_file}") + + # 7. Register form in parent object ChildObjects + register_form_in_object(type_name, obj_name, form_name) + + return [form_meta_file, form_xml_file, module_bsl_file] + + # --- 9. Parse -Object into items --- + items = [] + for part in args.Object.split(";;"): + trimmed = part.strip() + if trimmed: + items.append(trimmed) + + if not items: + print("No objects specified in -Object", file=sys.stderr) + sys.exit(1) + + # --- 10. Process each item --- + borrowed_files = [] + borrowed_count = 0 + + for item in items: + dot_idx = item.find(".") + if dot_idx < 1: + print(f"Invalid format '{item}', expected 'Type.Name' or 'Type.Name.Form.FormName'", file=sys.stderr) + sys.exit(1) + type_name = item[:dot_idx] + remainder = item[dot_idx + 1:] + + if type_name in SYNONYM_MAP: + type_name = SYNONYM_MAP[type_name] + + if type_name not in CHILD_TYPE_DIR_MAP: + print(f"Unknown type '{type_name}'", file=sys.stderr) + sys.exit(1) + + form_name = None + form_idx = remainder.find(".Form.") + if form_idx > 0: + obj_name = remainder[:form_idx] + form_name = remainder[form_idx + 6:] + else: + obj_name = remainder + + dir_name = CHILD_TYPE_DIR_MAP[type_name] + + if form_name: + # --- Form borrowing --- + info(f"Borrowing form {type_name}.{obj_name}.Form.{form_name}...") + + if not test_object_borrowed(type_name, obj_name): + info(f" Parent object {type_name}.{obj_name} not yet borrowed \u2014 borrowing first...") + + src = read_source_object(type_name, obj_name) + info(f" Source UUID: {src['Uuid']}") + borrowed_xml = build_borrowed_object_xml(type_name, obj_name, src["Uuid"], src["Properties"]) + + target_dir = os.path.join(ext_dir, dir_name) + os.makedirs(target_dir, exist_ok=True) + target_file = os.path.join(target_dir, f"{obj_name}.xml") + save_text_bom(target_file, borrowed_xml) + info(f" Created: {target_file}") + + add_to_child_objects(type_name, obj_name) + borrowed_files.append(target_file) + + form_files = borrow_form(type_name, obj_name, form_name) + borrowed_files.extend(form_files) + borrowed_count += 1 + else: + # --- Object borrowing --- + info(f"Borrowing {type_name}.{obj_name}...") + + src = read_source_object(type_name, obj_name) + info(f" Source UUID: {src['Uuid']}") + + borrowed_xml = build_borrowed_object_xml(type_name, obj_name, src["Uuid"], src["Properties"]) + + target_dir = os.path.join(ext_dir, dir_name) + os.makedirs(target_dir, exist_ok=True) + + target_file = os.path.join(target_dir, f"{obj_name}.xml") + save_text_bom(target_file, borrowed_xml) + info(f" Created: {target_file}") + + add_to_child_objects(type_name, obj_name) + + borrowed_files.append(target_file) + borrowed_count += 1 + + # --- Save modified Configuration.xml --- + save_xml_bom(tree, ext_resolved) + info(f"Saved: {ext_resolved}") + + # --- Summary --- + print() + print("=== cfe-borrow summary ===") + print(f" Extension: {ext_dir}") + print(f" Config: {cfg_dir}") + print(f" Borrowed: {borrowed_count} object(s)") + for f in borrowed_files: + print(f" - {f}") + sys.exit(0) + + +if __name__ == "__main__": + main() diff --git a/.claude/skills/cfe-diff/scripts/cfe-diff.py b/.claude/skills/cfe-diff/scripts/cfe-diff.py new file mode 100644 index 00000000..d2b3e166 --- /dev/null +++ b/.claude/skills/cfe-diff/scripts/cfe-diff.py @@ -0,0 +1,538 @@ +#!/usr/bin/env python3 +# cfe-diff v1.0 — Analyze and compare 1C configuration extension (CFE) +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills + +import argparse +import os +import re +import sys +from lxml import etree + +# --- Namespace maps --- + +MD_NSMAP = { + "md": "http://v8.1c.ru/8.3/MDClasses", + "xr": "http://v8.1c.ru/8.3/xcf/readable", +} + +FORM_NSMAP = { + "f": "http://v8.1c.ru/8.3/xcf/logform", +} + +# --- Type -> directory mapping --- + +CHILD_TYPE_DIR_MAP = { + "Catalog": "Catalogs", + "Document": "Documents", + "Enum": "Enums", + "CommonModule": "CommonModules", + "CommonPicture": "CommonPictures", + "CommonCommand": "CommonCommands", + "CommonTemplate": "CommonTemplates", + "ExchangePlan": "ExchangePlans", + "Report": "Reports", + "DataProcessor": "DataProcessors", + "InformationRegister": "InformationRegisters", + "AccumulationRegister": "AccumulationRegisters", + "ChartOfCharacteristicTypes": "ChartsOfCharacteristicTypes", + "ChartOfAccounts": "ChartsOfAccounts", + "AccountingRegister": "AccountingRegisters", + "ChartOfCalculationTypes": "ChartsOfCalculationTypes", + "CalculationRegister": "CalculationRegisters", + "BusinessProcess": "BusinessProcesses", + "Task": "Tasks", + "Subsystem": "Subsystems", + "Role": "Roles", + "Constant": "Constants", + "FunctionalOption": "FunctionalOptions", + "DefinedType": "DefinedTypes", + "FunctionalOptionsParameter": "FunctionalOptionsParameters", + "CommonForm": "CommonForms", + "DocumentJournal": "DocumentJournals", + "SessionParameter": "SessionParameters", + "StyleItem": "StyleItems", + "EventSubscription": "EventSubscriptions", + "ScheduledJob": "ScheduledJobs", + "SettingsStorage": "SettingsStorages", + "FilterCriterion": "FilterCriteria", + "CommandGroup": "CommandGroups", + "DocumentNumerator": "DocumentNumerators", + "Sequence": "Sequences", + "IntegrationService": "IntegrationServices", + "CommonAttribute": "CommonAttributes", +} + + +# --- Helper: check if object is borrowed --- + +def get_object_info(obj_type, obj_name, extension_path): + if obj_type not in CHILD_TYPE_DIR_MAP: + return None + dir_name = CHILD_TYPE_DIR_MAP[obj_type] + obj_file = os.path.join(extension_path, dir_name, f"{obj_name}.xml") + + if not os.path.isfile(obj_file): + return {"Borrowed": False, "File": obj_file, "Exists": False} + + parser_xml = etree.XMLParser(remove_blank_text=False) + doc = etree.parse(obj_file, parser_xml) + doc_root = doc.getroot() + + # Find first element child + obj_el = None + for c in doc_root: + if isinstance(c.tag, str): + obj_el = c + break + + if obj_el is None: + return {"Borrowed": False, "File": obj_file, "Exists": True} + + props_el = obj_el.find("md:Properties", MD_NSMAP) + ob_node = None + if props_el is not None: + ob_node = props_el.find("md:ObjectBelonging", MD_NSMAP) + + borrowed = ob_node is not None and ob_node.text == "Adopted" + + return { + "Borrowed": borrowed, + "File": obj_file, + "Exists": True, + "Type": obj_type, + "Name": obj_name, + "DirName": dir_name, + "ObjElement": obj_el, + } + + +# --- Helper: find .bsl files for object --- + +def get_bsl_files(obj_type, obj_name, extension_path): + if obj_type not in CHILD_TYPE_DIR_MAP: + return [] + dir_name = CHILD_TYPE_DIR_MAP[obj_type] + obj_dir = os.path.join(extension_path, dir_name, obj_name) + + if not os.path.isdir(obj_dir): + return [] + + bsl_files = [] + ext_dir = os.path.join(obj_dir, "Ext") + if os.path.isdir(ext_dir): + for item in os.listdir(ext_dir): + if item.lower().endswith(".bsl"): + bsl_files.append(os.path.join(ext_dir, item)) + + # Forms + forms_dir = os.path.join(obj_dir, "Forms") + if os.path.isdir(forms_dir): + for dirpath, dirnames, filenames in os.walk(forms_dir): + for fn in filenames: + if fn == "Module.bsl": + bsl_files.append(os.path.join(dirpath, fn)) + + return bsl_files + + +# --- Helper: parse interceptors from .bsl --- + +def get_interceptors(bsl_path): + if not os.path.isfile(bsl_path): + return [] + + with open(bsl_path, "r", encoding="utf-8-sig") as fh: + lines = fh.readlines() + + interceptors = [] + pattern = re.compile(r'^&(\u041f\u0435\u0440\u0435\u0434|\u041f\u043e\u0441\u043b\u0435|\u0418\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0435\u0418\u041a\u043e\u043d\u0442\u0440\u043e\u043b\u044c|\u0412\u043c\u0435\u0441\u0442\u043e)\("([^"]+)"\)') + # The above is: ^&(Перед|После|ИзменениеИКонтроль|Вместо)\("([^"]+)"\) + + for i, line in enumerate(lines): + stripped = line.strip() + m = pattern.match(stripped) + if m: + interceptors.append({ + "Type": m.group(1), + "Method": m.group(2), + "Line": i + 1, + "File": bsl_path, + }) + + return interceptors + + +# --- Helper: extract #Вставка blocks from .bsl --- + +def get_insertion_blocks(bsl_path): + if not os.path.isfile(bsl_path): + return [] + + with open(bsl_path, "r", encoding="utf-8-sig") as fh: + lines = fh.readlines() + + blocks = [] + in_block = False + block_lines = [] + start_line = 0 + + for i, line in enumerate(lines): + stripped = line.strip() + if stripped == "\u0023\u0412\u0441\u0442\u0430\u0432\u043a\u0430": + # #Вставка + in_block = True + block_lines = [] + start_line = i + 1 + elif stripped == "\u0023\u041a\u043e\u043d\u0435\u0446\u0412\u0441\u0442\u0430\u0432\u043a\u0438" and in_block: + # #КонецВставки + in_block = False + blocks.append({ + "StartLine": start_line, + "EndLine": i + 1, + "Code": "\n".join(block_lines).strip(), + "File": bsl_path, + }) + elif in_block: + block_lines.append(line.rstrip("\n").rstrip("\r")) + + return blocks + + +# --- Helper: analyze form for callType events and commands --- + +def get_form_interceptors(form_xml_path): + if not os.path.isfile(form_xml_path): + return None + + parser_xml = etree.XMLParser(remove_blank_text=False) + try: + doc = etree.parse(form_xml_path, parser_xml) + except Exception: + return None + + f_root = doc.getroot() + base_form = f_root.find("f:BaseForm", FORM_NSMAP) + is_borrowed = base_form is not None + + interceptors = [] + + # Form-level events with callType + events_node = f_root.find("f:Events", FORM_NSMAP) + if events_node is not None: + for evt in events_node.findall("f:Event", FORM_NSMAP): + ct = evt.get("callType", "") + if ct: + evt_name = evt.get("name", "") + evt_text = evt.text or "" + interceptors.append(f"Event:{evt_name} [{ct}] -> {evt_text}") + + # Element-level events with callType (scan all elements recursively) + child_items = f_root.find("f:ChildItems", FORM_NSMAP) + if child_items is not None: + # Walk all descendant elements looking for Events/Event[@callType] + f_ns = FORM_NSMAP["f"] + for el in child_items.iter(): + if not isinstance(el.tag, str): + continue + el_name = el.get("name", "") + if not el_name: + continue + events_sub = el.find(f"{{{f_ns}}}Events") + if events_sub is None: + continue + for evt in events_sub.findall(f"{{{f_ns}}}Event"): + ct = evt.get("callType", "") + if ct: + evt_name = evt.get("name", "") + evt_text = evt.text or "" + interceptors.append(f"Element:{el_name}.{evt_name} [{ct}] -> {evt_text}") + + # Commands with callType on Action + f_ns = FORM_NSMAP["f"] + cmds_node = f_root.find(f"{{{f_ns}}}Commands") + if cmds_node is not None: + for cmd in cmds_node.findall(f"{{{f_ns}}}Command"): + cmd_name = cmd.get("name", "") + for action in cmd.findall(f"{{{f_ns}}}Action"): + ct = action.get("callType", "") + if ct: + action_text = action.text or "" + interceptors.append(f"Command:{cmd_name} [{ct}] -> {action_text}") + + return { + "IsBorrowed": is_borrowed, + "Interceptors": interceptors, + } + + +# --- Mode A: Extension overview --- + +def mode_a(objects, extension_path): + borrowed_list = [] + own_list = [] + + for obj in objects: + info = get_object_info(obj["Type"], obj["Name"], extension_path) + if info is None: + print(f" [?] {obj['Type']}.{obj['Name']} \u2014 unknown type") + continue + if not info["Exists"]: + print(f" [?] {obj['Type']}.{obj['Name']} \u2014 file not found") + continue + + if info["Borrowed"]: + borrowed_list.append(obj) + + print(f" [BORROWED] {obj['Type']}.{obj['Name']}") + + # Find .bsl files and interceptors + bsl_files = get_bsl_files(obj["Type"], obj["Name"], extension_path) + for bsl in bsl_files: + rel_path = bsl.replace(extension_path, "").lstrip("\\/") + interceptor_list = get_interceptors(bsl) + if len(interceptor_list) > 0: + for ic in interceptor_list: + print(f' &{ic["Type"]}("{ic["Method"]}") \u2014 line {ic["Line"]} in {rel_path}') + else: + print(f" {rel_path} (no interceptors)") + + # Check for own attributes/forms in ChildObjects + obj_el = info.get("ObjElement") + if obj_el is not None: + child_obj = obj_el.find("md:ChildObjects", MD_NSMAP) + if child_obj is not None: + own_attrs = 0 + own_forms = 0 + own_ts = 0 + borrowed_items = 0 + form_names = [] + for c in child_obj: + if not isinstance(c.tag, str): + continue + ln = etree.QName(c.tag).localname + c_props = c.find("md:Properties", MD_NSMAP) + if c_props is not None: + c_ob = c_props.find("md:ObjectBelonging", MD_NSMAP) + if c_ob is not None and c_ob.text == "Adopted": + borrowed_items += 1 + continue + if ln == "Attribute": + own_attrs += 1 + elif ln == "TabularSection": + own_ts += 1 + elif ln == "Form": + form_names.append(c.text or "") + own_forms += 1 + + parts = [] + if own_attrs > 0: + parts.append(f"{own_attrs} own attrs") + if own_ts > 0: + parts.append(f"{own_ts} own TS") + if own_forms > 0: + parts.append(f"{own_forms} own forms") + if borrowed_items > 0: + parts.append(f"{borrowed_items} borrowed items") + if len(parts) > 0: + print(f" ChildObjects: {', '.join(parts)}") + + # Analyze forms + for fn in form_names: + form_xml_path = os.path.join( + extension_path, info["DirName"], info["Name"], + "Forms", fn, "Ext", "Form.xml" + ) + fi = get_form_interceptors(form_xml_path) + if fi is None: + print(f" Form.{fn} (?)") + continue + form_tag = "borrowed" if fi["IsBorrowed"] else "own" + if len(fi["Interceptors"]) > 0: + print(f" Form.{fn} ({form_tag}):") + for ic in fi["Interceptors"]: + print(f" {ic}") + else: + print(f" Form.{fn} ({form_tag})") + else: + own_list.append(obj) + print(f" [OWN] {obj['Type']}.{obj['Name']}") + + # Brief info for own objects + obj_el = info.get("ObjElement") + if obj_el is not None: + child_obj = obj_el.find("md:ChildObjects", MD_NSMAP) + if child_obj is not None: + attrs = 0 + forms = 0 + ts = 0 + for c in child_obj: + if not isinstance(c.tag, str): + continue + ln = etree.QName(c.tag).localname + if ln == "Attribute": + attrs += 1 + elif ln == "TabularSection": + ts += 1 + elif ln == "Form": + forms += 1 + parts = [] + if attrs > 0: + parts.append(f"{attrs} attrs") + if ts > 0: + parts.append(f"{ts} TS") + if forms > 0: + parts.append(f"{forms} forms") + if len(parts) > 0: + print(f" {', '.join(parts)}") + + print("") + print(f"=== Summary: {len(borrowed_list)} borrowed, {len(own_list)} own objects ===") + + +# --- Mode B: Transfer check --- + +def mode_b(objects, extension_path, config_path): + transferred = 0 + not_transferred = 0 + needs_review = 0 + + for obj in objects: + info = get_object_info(obj["Type"], obj["Name"], extension_path) + if info is None or not info["Exists"] or not info["Borrowed"]: + continue + + # Find .bsl files with &ИзменениеИКонтроль + bsl_files = get_bsl_files(obj["Type"], obj["Name"], extension_path) + for bsl in bsl_files: + interceptor_list = get_interceptors(bsl) + mac_interceptors = [ic for ic in interceptor_list if ic["Type"] == "\u0418\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0435\u0418\u041a\u043e\u043d\u0442\u0440\u043e\u043b\u044c"] + + if len(mac_interceptors) == 0: + continue + + for ic in mac_interceptors: + method_name = ic["Method"] + rel_bsl = bsl.replace(extension_path, "").lstrip("\\/") + + # Find #Вставка blocks in this file + insert_blocks = get_insertion_blocks(bsl) + + if len(insert_blocks) == 0: + print(f' [NEEDS_REVIEW] {obj["Type"]}.{obj["Name"]} \u2014 &\u0418\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0435\u0418\u041a\u043e\u043d\u0442\u0440\u043e\u043b\u044c("{method_name}") \u2014 no #\u0412\u0441\u0442\u0430\u0432\u043a\u0430 blocks') + needs_review += 1 + continue + + # Find corresponding module in config + if obj["Type"] not in CHILD_TYPE_DIR_MAP: + continue + config_bsl = bsl.replace(extension_path, config_path) + + if not os.path.isfile(config_bsl): + print(f' [NEEDS_REVIEW] {obj["Type"]}.{obj["Name"]} \u2014 &\u0418\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0435\u0418\u041a\u043e\u043d\u0442\u0440\u043e\u043b\u044c("{method_name}") \u2014 config module not found') + needs_review += 1 + continue + + with open(config_bsl, "r", encoding="utf-8-sig") as fh: + config_content = fh.read() + + all_transferred = True + for block in insert_blocks: + code = block["Code"] + if not code: + continue + + # Normalize whitespace for comparison + code_norm = re.sub(r'\s+', ' ', code) + config_norm = re.sub(r'\s+', ' ', config_content) + + if code_norm not in config_norm: + all_transferred = False + + if all_transferred: + print(f' [TRANSFERRED] {obj["Type"]}.{obj["Name"]} \u2014 &\u0418\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0435\u0418\u041a\u043e\u043d\u0442\u0440\u043e\u043b\u044c("{method_name}") \u2014 {len(insert_blocks)} block(s)') + transferred += 1 + else: + print(f' [NOT_TRANSFERRED] {obj["Type"]}.{obj["Name"]} \u2014 &\u0418\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0435\u0418\u041a\u043e\u043d\u0442\u0440\u043e\u043b\u044c("{method_name}") \u2014 some blocks not found in config') + not_transferred += 1 + + print("") + print(f"=== Transfer check: {transferred} transferred, {not_transferred} not transferred, {needs_review} needs review ===") + + +# --- Main --- + +def main(): + parser = argparse.ArgumentParser(description="Analyze and compare 1C configuration extension (CFE)", allow_abbrev=False) + parser.add_argument("-ExtensionPath", required=True, help="Path to extension dump root") + parser.add_argument("-ConfigPath", required=True, help="Path to base config dump root") + parser.add_argument("-Mode", choices=["A", "B"], default="A", help="A=overview, B=transfer check") + args = parser.parse_args() + + extension_path = args.ExtensionPath + config_path = args.ConfigPath + mode = args.Mode + + # --- Resolve paths --- + if not os.path.isabs(extension_path): + extension_path = os.path.join(os.getcwd(), extension_path) + if not os.path.isabs(config_path): + config_path = os.path.join(os.getcwd(), config_path) + if os.path.isfile(extension_path): + extension_path = os.path.dirname(extension_path) + if os.path.isfile(config_path): + config_path = os.path.dirname(config_path) + + ext_cfg = os.path.join(extension_path, "Configuration.xml") + src_cfg = os.path.join(config_path, "Configuration.xml") + if not os.path.isfile(ext_cfg): + print(f"Extension Configuration.xml not found: {ext_cfg}", file=sys.stderr) + sys.exit(1) + if not os.path.isfile(src_cfg): + print(f"Config Configuration.xml not found: {src_cfg}", file=sys.stderr) + sys.exit(1) + + # --- Parse extension Configuration.xml --- + parser_xml = etree.XMLParser(remove_blank_text=False) + ext_doc = etree.parse(ext_cfg, parser_xml) + ext_root = ext_doc.getroot() + + ext_props = ext_root.find(".//md:Configuration/md:Properties", MD_NSMAP) + ext_name_node = ext_props.find("md:Name", MD_NSMAP) if ext_props is not None else None + ext_name = ext_name_node.text if ext_name_node is not None and ext_name_node.text else "?" + prefix_node = ext_props.find("md:NamePrefix", MD_NSMAP) if ext_props is not None else None + name_prefix = prefix_node.text if prefix_node is not None and prefix_node.text else "" + purpose_node = ext_props.find("md:ConfigurationExtensionPurpose", MD_NSMAP) if ext_props is not None else None + purpose = purpose_node.text if purpose_node is not None and purpose_node.text else "?" + + print(f"=== cfe-diff Mode {mode}: {ext_name} ({purpose}) ===") + print(f" NamePrefix: {name_prefix}") + print("") + + # --- Collect ChildObjects --- + child_obj_node = ext_root.find(".//md:Configuration/md:ChildObjects", MD_NSMAP) + if child_obj_node is None: + print("[WARN] No ChildObjects in extension") + sys.exit(0) + + objects = [] + for child in child_obj_node: + if not isinstance(child.tag, str): + continue + ln = etree.QName(child.tag).localname + if ln == "Language": + continue + objects.append({"Type": ln, "Name": child.text or ""}) + + if len(objects) == 0: + print("No objects (besides Language) in extension.") + sys.exit(0) + + # --- Run selected mode --- + if mode == "A": + mode_a(objects, extension_path) + elif mode == "B": + mode_b(objects, extension_path, config_path) + + +if __name__ == "__main__": + main() diff --git a/.claude/skills/cfe-init/scripts/cfe-init.py b/.claude/skills/cfe-init/scripts/cfe-init.py new file mode 100644 index 00000000..9fae04ac --- /dev/null +++ b/.claude/skills/cfe-init/scripts/cfe-init.py @@ -0,0 +1,237 @@ +#!/usr/bin/env python3 +# cfe-init v1.0 — Create 1C configuration extension scaffold (CFE) +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +"""Generates minimal XML source files for a 1C configuration extension.""" +import sys, os, argparse, uuid +from xml.etree import ElementTree as ET + +def esc_xml(s): + return s.replace('&','&').replace('<','<').replace('>','>').replace('"','"') + +def new_uuid(): + return str(uuid.uuid4()) + +def write_utf8_bom(path, content): + with open(path, 'w', encoding='utf-8-sig', newline='') as f: + f.write(content) + +def main(): + parser = argparse.ArgumentParser(description='Create 1C configuration extension scaffold', allow_abbrev=False) + parser.add_argument('-Name', dest='Name', required=True) + parser.add_argument('-Synonym', dest='Synonym', default=None) + parser.add_argument('-NamePrefix', dest='NamePrefix', default=None) + parser.add_argument('-OutputDir', dest='OutputDir', default='src') + parser.add_argument('-Purpose', dest='Purpose', default='Customization', choices=['Patch','Customization','AddOn']) + parser.add_argument('-Version', dest='Version', default='') + parser.add_argument('-Vendor', dest='Vendor', default='') + parser.add_argument('-CompatibilityMode', dest='CompatibilityMode', default='Version8_3_24') + parser.add_argument('-ConfigPath', dest='ConfigPath', default=None) + parser.add_argument('-NoRole', dest='NoRole', action='store_true') + args = parser.parse_args() + + name = args.Name + synonym = args.Synonym if args.Synonym else name + name_prefix = args.NamePrefix if args.NamePrefix else f"{name}_" + output_dir = args.OutputDir + purpose = args.Purpose + version = args.Version + vendor = args.Vendor + compat = args.CompatibilityMode + + # --- Resolve output dir --- + if not os.path.isabs(output_dir): + output_dir = os.path.join(os.getcwd(), output_dir) + + # --- Check existing --- + cfg_file = os.path.join(output_dir, "Configuration.xml") + if os.path.exists(cfg_file): + print(f"Configuration.xml already exists: {cfg_file}", file=sys.stderr) + sys.exit(1) + + # --- Resolve ConfigPath --- + base_lang_uuid = "00000000-0000-0000-0000-000000000000" + if args.ConfigPath: + config_path = args.ConfigPath + if not os.path.isabs(config_path): + config_path = os.path.join(os.getcwd(), config_path) + if os.path.isdir(config_path): + candidate = os.path.join(config_path, "Configuration.xml") + if os.path.exists(candidate): + config_path = candidate + else: + print(f"No Configuration.xml in config directory: {config_path}", file=sys.stderr) + sys.exit(1) + if not os.path.exists(config_path): + print(f"Config file not found: {config_path}", file=sys.stderr) + sys.exit(1) + cfg_dir = os.path.dirname(os.path.abspath(config_path)) + + # Read Language UUID from base config + base_lang_file = os.path.join(cfg_dir, "Languages", "Русский.xml") + if os.path.exists(base_lang_file): + try: + base_tree = ET.parse(base_lang_file) + base_root = base_tree.getroot() + for child in base_root: + if child.tag.endswith('}Language') or child.tag == 'Language': + base_lang_uuid = child.get('uuid', base_lang_uuid) + print(f"[INFO] Base config Language UUID: {base_lang_uuid}") + break + except Exception: + print(f"[WARN] Could not parse {base_lang_file}") + else: + print(f"[WARN] Base config language not found: {base_lang_file}") + + # Read CompatibilityMode from base config + try: + base_cfg_tree = ET.parse(os.path.abspath(config_path)) + base_cfg_root = base_cfg_tree.getroot() + ns = {'md': 'http://v8.1c.ru/8.3/MDClasses'} + compat_node = base_cfg_root.find('.//md:Configuration/md:Properties/md:CompatibilityMode', ns) + if compat_node is not None and compat_node.text: + compat = compat_node.text.strip() + print(f"[INFO] Base config CompatibilityMode: {compat}") + else: + print(f"[WARN] CompatibilityMode not found in base config, using default: {compat}") + except Exception: + print(f"[WARN] Could not parse base config, using default CompatibilityMode: {compat}") + else: + print("[WARN] Language ExtendedConfigurationObject set to zeros. Use -ConfigPath to auto-resolve from base config, or fix manually before loading.") + + # --- Generate UUIDs --- + uuid_cfg = new_uuid() + uuid_lang = new_uuid() + uuid_role = new_uuid() + co = [new_uuid() for _ in range(7)] + + # --- Synonym XML --- + synonym_xml = "" + if synonym: + synonym_xml = f"\r\n\t\t\t\t\r\n\t\t\t\t\tru\r\n\t\t\t\t\t{esc_xml(synonym)}\r\n\t\t\t\t\r\n\t\t\t" + + vendor_xml = esc_xml(vendor) if vendor else "" + version_xml = esc_xml(version) if version else "" + + # --- Role name --- + role_name = f"{name_prefix}ОсновнаяРоль" + + # --- DefaultRoles XML --- + default_roles_xml = "" + if not args.NoRole: + default_roles_xml = f'\r\n\t\t\t\tRole.{role_name}\r\n\t\t\t' + + # --- ChildObjects --- + child_objects_xml = f"\r\n\t\t\tРусский" + if not args.NoRole: + child_objects_xml += f"\r\n\t\t\t{role_name}" + child_objects_xml += "\r\n\t\t" + + class_ids = [ + "9cd510cd-abfc-11d4-9434-004095e12fc7", + "9fcd25a0-4822-11d4-9414-008048da11f9", + "e3687481-0a87-462c-a166-9f34594f9bba", + "9de14907-ec23-4a07-96f0-85521cb6b53b", + "51f2d5d8-ea4d-4064-8892-82951750031e", + "e68182ea-4237-4383-967f-90c1e3370bc7", + "fb282519-d103-4dd3-bc12-cb271d631dfc", + ] + + contained_objects = "" + for i in range(7): + contained_objects += f"""\t\t\t +\t\t\t\t{class_ids[i]} +\t\t\t\t{co[i]} +\t\t\t\n""" + + cfg_xml = f''' + +\t +\t\t +{contained_objects}\t\t +\t\t +\t\t\tAdopted +\t\t\t{esc_xml(name)} +\t\t\t{synonym_xml} +\t\t\t +\t\t\t{purpose} +\t\t\ttrue +\t\t\t{esc_xml(name_prefix)} +\t\t\t{compat} +\t\t\tManagedApplication +\t\t\t +\t\t\t\tPlatformApplication +\t\t\t +\t\t\tRussian +\t\t\t{default_roles_xml} +\t\t\t{vendor_xml} +\t\t\t{version_xml} +\t\t\tLanguage.Русский +\t\t\t +\t\t\t +\t\t\t +\t\t\t +\t\t\t +\t\t\tTaxiEnableVersion8_2 +\t\t +\t\t{child_objects_xml} +\t +''' + + # --- Languages/Русский.xml (adopted format) --- + lang_xml = f''' + +\t +\t\t +\t\t +\t\t\tAdopted +\t\t\tРусский +\t\t\t +\t\t\t{base_lang_uuid} +\t\t\tru +\t\t +\t +''' + + # --- Role XML --- + role_xml = f''' + +\t +\t\t +\t\t\t{esc_xml(role_name)} +\t\t\t +\t\t\t +\t\t +\t +''' + + # --- Create directories --- + os.makedirs(output_dir, exist_ok=True) + lang_dir = os.path.join(output_dir, "Languages") + os.makedirs(lang_dir, exist_ok=True) + + # --- Write files --- + write_utf8_bom(cfg_file, cfg_xml) + lang_file = os.path.join(lang_dir, "Русский.xml") + write_utf8_bom(lang_file, lang_xml) + + # --- Role --- + role_file = None + if not args.NoRole: + role_dir = os.path.join(output_dir, "Roles") + os.makedirs(role_dir, exist_ok=True) + role_file = os.path.join(role_dir, f"{role_name}.xml") + write_utf8_bom(role_file, role_xml) + + # --- Output --- + print(f"[OK] Создано расширение: {name}") + print(f" Каталог: {output_dir}") + print(f" Назначение: {purpose}") + print(f" Префикс: {name_prefix}") + print(f" Совместимость: {compat}") + print(f" Configuration.xml: {cfg_file}") + print(f" Languages: {lang_file}") + if role_file: + print(f" Role: {role_file}") + +if __name__ == '__main__': + main() diff --git a/.claude/skills/cfe-patch-method/scripts/cfe-patch-method.py b/.claude/skills/cfe-patch-method/scripts/cfe-patch-method.py new file mode 100644 index 00000000..7c66f8fc --- /dev/null +++ b/.claude/skills/cfe-patch-method/scripts/cfe-patch-method.py @@ -0,0 +1,229 @@ +#!/usr/bin/env python3 +# cfe-patch-method v1.0 — Generate method interceptor for 1C extension (CFE) +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills + +import argparse +import os +import sys +import xml.etree.ElementTree as ET + + +def main(): + parser = argparse.ArgumentParser( + description="Generate method interceptor for 1C extension (CFE)", + allow_abbrev=False, + ) + parser.add_argument("-ExtensionPath", required=True) + parser.add_argument("-ModulePath", required=True) + parser.add_argument("-MethodName", required=True) + parser.add_argument( + "-InterceptorType", + required=True, + choices=["Before", "After", "ModificationAndControl"], + ) + parser.add_argument("-Context", default="\u041d\u0430\u0421\u0435\u0440\u0432\u0435\u0440\u0435") # НаСервере + parser.add_argument("-IsFunction", action="store_true") + args = parser.parse_args() + + extension_path = args.ExtensionPath + module_path = args.ModulePath + method_name = args.MethodName + interceptor_type = args.InterceptorType + context = args.Context + is_function = args.IsFunction + + # --- Resolve extension path --- + if not os.path.isabs(extension_path): + extension_path = os.path.join(os.getcwd(), extension_path) + if os.path.isfile(extension_path): + extension_path = os.path.dirname(extension_path) + + cfg_file = os.path.join(extension_path, "Configuration.xml") + if not os.path.isfile(cfg_file): + print(f"Configuration.xml not found in: {extension_path}", file=sys.stderr) + sys.exit(1) + + # --- Read NamePrefix from Configuration.xml --- + tree = ET.parse(cfg_file) + root = tree.getroot() + + ns = {"md": "http://v8.1c.ru/8.3/MDClasses"} + props_node = root.find(".//md:Configuration/md:Properties", ns) + name_prefix = "\u0420\u0430\u0441\u0448_" # Расш_ + if props_node is not None: + prefix_node = props_node.find("md:NamePrefix", ns) + if prefix_node is not None and prefix_node.text: + name_prefix = prefix_node.text + + # --- Map ModulePath to file path --- + # ModulePath formats: + # Catalog.X.ObjectModule -> Catalogs/X/Ext/ObjectModule.bsl + # Catalog.X.ManagerModule -> Catalogs/X/Ext/ManagerModule.bsl + # Catalog.X.Form.Y -> Catalogs/X/Forms/Y/Ext/Form/Module.bsl + # CommonModule.X -> CommonModules/X/Ext/Module.bsl + # Document.X.ObjectModule -> Documents/X/Ext/ObjectModule.bsl + # Document.X.ManagerModule -> Documents/X/Ext/ManagerModule.bsl + # Document.X.Form.Y -> Documents/X/Forms/Y/Ext/Form/Module.bsl + + type_dir_map = { + "Catalog": "Catalogs", + "Document": "Documents", + "Enum": "Enums", + "CommonModule": "CommonModules", + "Report": "Reports", + "DataProcessor": "DataProcessors", + "ExchangePlan": "ExchangePlans", + "ChartOfAccounts": "ChartsOfAccounts", + "ChartOfCharacteristicTypes": "ChartsOfCharacteristicTypes", + "ChartOfCalculationTypes": "ChartsOfCalculationTypes", + "BusinessProcess": "BusinessProcesses", + "Task": "Tasks", + "InformationRegister": "InformationRegisters", + "AccumulationRegister": "AccumulationRegisters", + "AccountingRegister": "AccountingRegisters", + "CalculationRegister": "CalculationRegisters", + } + + parts = module_path.split(".") + if len(parts) < 2: + print( + f"Invalid ModulePath format: {module_path}. " + "Expected: Type.Name.Module or CommonModule.Name", + file=sys.stderr, + ) + sys.exit(1) + + obj_type = parts[0] + obj_name = parts[1] + + if obj_type not in type_dir_map: + print(f"Unknown object type: {obj_type}", file=sys.stderr) + sys.exit(1) + + dir_name = type_dir_map[obj_type] + + bsl_file = None + if obj_type == "CommonModule": + # CommonModule.X -> CommonModules/X/Ext/Module.bsl + bsl_file = os.path.join(extension_path, dir_name, obj_name, "Ext", "Module.bsl") + elif len(parts) >= 4 and parts[2] == "Form": + # Type.X.Form.Y -> Types/X/Forms/Y/Ext/Form/Module.bsl + form_name = parts[3] + bsl_file = os.path.join( + extension_path, dir_name, obj_name, "Forms", form_name, "Ext", "Form", "Module.bsl" + ) + elif len(parts) >= 3: + # Type.X.ObjectModule -> Types/X/Ext/ObjectModule.bsl + module_name = parts[2] + module_file_map = { + "ObjectModule": "ObjectModule.bsl", + "ManagerModule": "ManagerModule.bsl", + "RecordSetModule": "RecordSetModule.bsl", + "CommandModule": "CommandModule.bsl", + } + module_file_name = module_file_map.get(module_name, f"{module_name}.bsl") + bsl_file = os.path.join(extension_path, dir_name, obj_name, "Ext", module_file_name) + else: + print( + f"Invalid ModulePath format: {module_path}. " + "Expected: Type.Name.Module, Type.Name.Form.FormName, or CommonModule.Name", + file=sys.stderr, + ) + sys.exit(1) + + # --- Map InterceptorType to decorator --- + decorator_map = { + "Before": "&\u041f\u0435\u0440\u0435\u0434", # &Перед + "After": "&\u041f\u043e\u0441\u043b\u0435", # &После + "ModificationAndControl": "&\u0418\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0435\u0418\u041a\u043e\u043d\u0442\u0440\u043e\u043b\u044c", # &ИзменениеИКонтроль + } + decorator = decorator_map[interceptor_type] + + # --- Map Context to annotation --- + context_map = { + "\u041d\u0430\u0421\u0435\u0440\u0432\u0435\u0440\u0435": "&\u041d\u0430\u0421\u0435\u0440\u0432\u0435\u0440\u0435", # НаСервере -> &НаСервере + "\u041d\u0430\u041a\u043b\u0438\u0435\u043d\u0442\u0435": "&\u041d\u0430\u041a\u043b\u0438\u0435\u043d\u0442\u0435", # НаКлиенте -> &НаКлиенте + "\u041d\u0430\u0421\u0435\u0440\u0432\u0435\u0440\u0435\u0411\u0435\u0437\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u0430": "&\u041d\u0430\u0421\u0435\u0440\u0432\u0435\u0440\u0435\u0411\u0435\u0437\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u0430", # НаСервереБезКонтекста -> &НаСервереБезКонтекста + } + context_annotation = context_map.get(context, f"&{context}") + + # --- Procedure name --- + proc_name = f"{name_prefix}{method_name}" + + # --- Generate BSL code --- + keyword = "\u0424\u0443\u043d\u043a\u0446\u0438\u044f" if is_function else "\u041f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u0430" # Функция / Процедура + end_keyword = "\u041a\u043e\u043d\u0435\u0446\u0424\u0443\u043d\u043a\u0446\u0438\u0438" if is_function else "\u041a\u043e\u043d\u0435\u0446\u041f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u044b" # КонецФункции / КонецПроцедуры + + body_lines = [] + if interceptor_type == "Before": + body_lines.append("\t// TODO: \u043a\u043e\u0434 \u043f\u0435\u0440\u0435\u0434 \u0432\u044b\u0437\u043e\u0432\u043e\u043c \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u043c\u0435\u0442\u043e\u0434\u0430") # код перед вызовом оригинального метода + elif interceptor_type == "After": + body_lines.append("\t// TODO: \u043a\u043e\u0434 \u043f\u043e\u0441\u043b\u0435 \u0432\u044b\u0437\u043e\u0432\u0430 \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u043c\u0435\u0442\u043e\u0434\u0430") # код после вызова оригинального метода + elif interceptor_type == "ModificationAndControl": + body_lines.append("\t// \u0421\u043a\u043e\u043f\u0438\u0440\u0443\u0439\u0442\u0435 \u0442\u0435\u043b\u043e \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u043c\u0435\u0442\u043e\u0434\u0430 \u0438 \u0432\u043d\u0435\u0441\u0438\u0442\u0435 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u044f,") # Скопируйте тело оригинального метода и внесите изменения, + body_lines.append("\t// \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f \u043c\u0430\u0440\u043a\u0435\u0440\u044b #\u0423\u0434\u0430\u043b\u0435\u043d\u0438\u0435 / #\u041a\u043e\u043d\u0435\u0446\u0423\u0434\u0430\u043b\u0435\u043d\u0438\u044f \u0438 #\u0412\u0441\u0442\u0430\u0432\u043a\u0430 / #\u041a\u043e\u043d\u0435\u0446\u0412\u0441\u0442\u0430\u0432\u043a\u0438") # используя маркеры #Удаление / #КонецУдаления и #Вставка / #КонецВставки + + if is_function: + body_lines.append("\t") + body_lines.append("\t\u0412\u043e\u0437\u0432\u0440\u0430\u0442 \u041d\u0435\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u043e; // TODO: \u0437\u0430\u043c\u0435\u043d\u0438\u0442\u044c \u043d\u0430 \u0440\u0435\u0430\u043b\u044c\u043d\u043e\u0435 \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u043c\u043e\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435") # Возврат Неопределено; // TODO: заменить на реальное возвращаемое значение + + bsl_code = [ + context_annotation, + f'{decorator}("{method_name}")', + f"{keyword} {proc_name}()", + ] + bsl_code.extend(body_lines) + bsl_code.append(end_keyword) + + bsl_text = "\r\n".join(bsl_code) + "\r\n" + + # --- Check form borrowing for .Form. paths --- + if len(parts) >= 4 and parts[2] == "Form": + form_name = parts[3] + form_meta_file = os.path.join( + extension_path, dir_name, obj_name, "Forms", f"{form_name}.xml" + ) + form_xml_file = os.path.join( + extension_path, dir_name, obj_name, "Forms", form_name, "Ext", "Form.xml" + ) + + if not os.path.isfile(form_meta_file) or not os.path.isfile(form_xml_file): + print(f"[WARN] Form '{form_name}' metadata or Form.xml not found in extension.") + print(" Run /cfe-borrow first:") + print( + f" /cfe-borrow -ExtensionPath {extension_path} " + f'-ConfigPath -Object "{obj_type}.{obj_name}.Form.{form_name}"' + ) + print() + + # --- Check if file exists and append --- + bsl_dir = os.path.dirname(bsl_file) + if not os.path.isdir(bsl_dir): + os.makedirs(bsl_dir, exist_ok=True) + + if os.path.isfile(bsl_file): + # Append to existing file + with open(bsl_file, "r", encoding="utf-8-sig") as f: + existing = f.read() + + separator = "\r\n" + if existing and not existing.endswith("\n"): + separator = "\r\n\r\n" + new_content = existing + separator + bsl_text + + with open(bsl_file, "w", encoding="utf-8-sig") as f: + f.write(new_content) + print("[OK] \u0414\u043e\u0431\u0430\u0432\u043b\u0435\u043d \u043f\u0435\u0440\u0435\u0445\u0432\u0430\u0442\u0447\u0438\u043a \u0432 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u0439 \u0444\u0430\u0439\u043b") # Добавлен перехватчик в существующий файл + else: + with open(bsl_file, "w", encoding="utf-8-sig") as f: + f.write(bsl_text) + print("[OK] \u0421\u043e\u0437\u0434\u0430\u043d \u0444\u0430\u0439\u043b \u043c\u043e\u0434\u0443\u043b\u044f") # Создан файл модуля + + print(f" \u0424\u0430\u0439\u043b: {bsl_file}") # Файл: + print(f' \u0414\u0435\u043a\u043e\u0440\u0430\u0442\u043e\u0440: {decorator}("{method_name}")') # Декоратор: + print(f" \u041f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u0430: {proc_name}()") # Процедура: + print(f" \u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442: {context_annotation}") # Контекст: + + +if __name__ == "__main__": + main() diff --git a/.claude/skills/cfe-validate/scripts/cfe-validate.py b/.claude/skills/cfe-validate/scripts/cfe-validate.py new file mode 100644 index 00000000..4a0ce566 --- /dev/null +++ b/.claude/skills/cfe-validate/scripts/cfe-validate.py @@ -0,0 +1,594 @@ +#!/usr/bin/env python3 +# cfe-validate v1.0 — Validate 1C configuration extension XML structure (CFE) +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +"""Validates extension Configuration.xml: root, InternalInfo, extension properties, ChildObjects, borrowed objects.""" +import sys, os, argparse, re +from lxml import etree + +NS = { + 'md': 'http://v8.1c.ru/8.3/MDClasses', + 'v8': 'http://v8.1c.ru/8.1/data/core', + 'xr': 'http://v8.1c.ru/8.3/xcf/readable', + 'xsi': 'http://www.w3.org/2001/XMLSchema-instance', + 'xs': 'http://www.w3.org/2001/XMLSchema', + 'app': 'http://v8.1c.ru/8.2/managed-application/core', +} + +GUID_PATTERN = re.compile( + r'^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$' +) +IDENT_PATTERN = re.compile( + r'^[A-Za-z\u0410-\u042F\u0401\u0430-\u044F\u0451_]' + r'[A-Za-z0-9\u0410-\u042F\u0401\u0430-\u044F\u0451_]*$' +) + +# 7 fixed ClassIds for Configuration +VALID_CLASS_IDS = [ + '9cd510cd-abfc-11d4-9434-004095e12fc7', + '9fcd25a0-4822-11d4-9414-008048da11f9', + 'e3687481-0a87-462c-a166-9f34594f9bba', + '9de14907-ec23-4a07-96f0-85521cb6b53b', + '51f2d5d8-ea4d-4064-8892-82951750031e', + 'e68182ea-4237-4383-967f-90c1e3370bc7', + 'fb282519-d103-4dd3-bc12-cb271d631dfc', +] + +# 44 types in canonical order +CHILD_OBJECT_TYPES = [ + 'Language', 'Subsystem', 'StyleItem', 'Style', + 'CommonPicture', 'SessionParameter', 'Role', 'CommonTemplate', + 'FilterCriterion', 'CommonModule', 'CommonAttribute', 'ExchangePlan', + 'XDTOPackage', 'WebService', 'HTTPService', 'WSReference', + 'EventSubscription', 'ScheduledJob', 'SettingsStorage', 'FunctionalOption', + 'FunctionalOptionsParameter', 'DefinedType', 'CommonCommand', 'CommandGroup', + 'Constant', 'CommonForm', 'Catalog', 'Document', + 'DocumentNumerator', 'Sequence', 'DocumentJournal', 'Enum', + 'Report', 'DataProcessor', 'InformationRegister', 'AccumulationRegister', + 'ChartOfCharacteristicTypes', 'ChartOfAccounts', 'AccountingRegister', + 'ChartOfCalculationTypes', 'CalculationRegister', + 'BusinessProcess', 'Task', 'IntegrationService', +] + +# Type -> directory mapping +CHILD_TYPE_DIR_MAP = { + 'Language': 'Languages', 'Subsystem': 'Subsystems', 'StyleItem': 'StyleItems', 'Style': 'Styles', + 'CommonPicture': 'CommonPictures', 'SessionParameter': 'SessionParameters', 'Role': 'Roles', + 'CommonTemplate': 'CommonTemplates', 'FilterCriterion': 'FilterCriteria', 'CommonModule': 'CommonModules', + 'CommonAttribute': 'CommonAttributes', 'ExchangePlan': 'ExchangePlans', 'XDTOPackage': 'XDTOPackages', + 'WebService': 'WebServices', 'HTTPService': 'HTTPServices', 'WSReference': 'WSReferences', + 'EventSubscription': 'EventSubscriptions', 'ScheduledJob': 'ScheduledJobs', + 'SettingsStorage': 'SettingsStorages', 'FunctionalOption': 'FunctionalOptions', + 'FunctionalOptionsParameter': 'FunctionalOptionsParameters', 'DefinedType': 'DefinedTypes', + 'CommonCommand': 'CommonCommands', 'CommandGroup': 'CommandGroups', 'Constant': 'Constants', + 'CommonForm': 'CommonForms', 'Catalog': 'Catalogs', 'Document': 'Documents', + 'DocumentNumerator': 'DocumentNumerators', 'Sequence': 'Sequences', + 'DocumentJournal': 'DocumentJournals', 'Enum': 'Enums', 'Report': 'Reports', + 'DataProcessor': 'DataProcessors', 'InformationRegister': 'InformationRegisters', + 'AccumulationRegister': 'AccumulationRegisters', + 'ChartOfCharacteristicTypes': 'ChartsOfCharacteristicTypes', + 'ChartOfAccounts': 'ChartsOfAccounts', 'AccountingRegister': 'AccountingRegisters', + 'ChartOfCalculationTypes': 'ChartsOfCalculationTypes', + 'CalculationRegister': 'CalculationRegisters', + 'BusinessProcess': 'BusinessProcesses', 'Task': 'Tasks', + 'IntegrationService': 'IntegrationServices', +} + +# Valid enum values for extension properties +VALID_ENUM_VALUES = { + 'ConfigurationExtensionCompatibilityMode': [ + 'DontUse', 'Version8_1', 'Version8_2_13', 'Version8_2_16', + 'Version8_3_1', 'Version8_3_2', 'Version8_3_3', 'Version8_3_4', 'Version8_3_5', + 'Version8_3_6', 'Version8_3_7', 'Version8_3_8', 'Version8_3_9', 'Version8_3_10', + 'Version8_3_11', 'Version8_3_12', 'Version8_3_13', 'Version8_3_14', 'Version8_3_15', + 'Version8_3_16', 'Version8_3_17', 'Version8_3_18', 'Version8_3_19', 'Version8_3_20', + 'Version8_3_21', 'Version8_3_22', 'Version8_3_23', 'Version8_3_24', 'Version8_3_25', + 'Version8_3_26', 'Version8_3_27', 'Version8_3_28', + ], + 'DefaultRunMode': ['ManagedApplication', 'OrdinaryApplication', 'Auto'], + 'ScriptVariant': ['Russian', 'English'], + 'InterfaceCompatibilityMode': ['Taxi', 'TaxiEnableVersion8_2', 'Version8_2'], +} + +EXPECTED_NS = 'http://v8.1c.ru/8.3/MDClasses' + + +class Reporter: + def __init__(self, max_errors): + self.errors = 0 + self.warnings = 0 + self.stopped = False + self.max_errors = max_errors + self.lines = [] + + def out(self, msg=''): + self.lines.append(msg) + + def ok(self, msg): + self.lines.append(f'[OK] {msg}') + + def error(self, msg): + self.errors += 1 + self.lines.append(f'[ERROR] {msg}') + if self.errors >= self.max_errors: + self.stopped = True + + def warn(self, msg): + self.warnings += 1 + self.lines.append(f'[WARN] {msg}') + + def text(self): + return '\r\n'.join(self.lines) + '\r\n' + + def finalize(self, out_file): + self.out('') + self.out(f'=== Result: {self.errors} errors, {self.warnings} warnings ===') + + result = self.text() + print(result, end='') + + if out_file: + with open(out_file, 'w', encoding='utf-8-sig', newline='') as f: + f.write(result) + print(f'Written to: {out_file}') + + +def main(): + parser = argparse.ArgumentParser( + description='Validate 1C configuration extension XML structure (CFE)', allow_abbrev=False + ) + parser.add_argument('-ExtensionPath', dest='ExtensionPath', required=True) + parser.add_argument('-MaxErrors', dest='MaxErrors', type=int, default=30) + parser.add_argument('-OutFile', dest='OutFile', default='') + args = parser.parse_args() + + extension_path = args.ExtensionPath + max_errors = args.MaxErrors + out_file = args.OutFile + + # --- Resolve path --- + if not os.path.isabs(extension_path): + extension_path = os.path.join(os.getcwd(), extension_path) + + if os.path.isdir(extension_path): + candidate = os.path.join(extension_path, 'Configuration.xml') + if os.path.exists(candidate): + extension_path = candidate + else: + print(f'[ERROR] No Configuration.xml found in directory: {extension_path}') + sys.exit(1) + + if not os.path.exists(extension_path): + print(f'[ERROR] File not found: {extension_path}') + sys.exit(1) + + resolved_path = os.path.abspath(extension_path) + config_dir = os.path.dirname(resolved_path) + + if out_file and not os.path.isabs(out_file): + out_file = os.path.join(os.getcwd(), out_file) + + r = Reporter(max_errors) + r.out('') + + # --- 1. Parse XML --- + xml_doc = None + try: + xml_parser = etree.XMLParser(remove_blank_text=False) + xml_doc = etree.parse(resolved_path, xml_parser) + except etree.XMLSyntaxError as e: + r.lines.insert(0, '=== Validation: Extension (parse failed) ===') + r.out('') + r.error(f'1. XML parse failed: {e}') + r.finalize(out_file) + sys.exit(1) + + root = xml_doc.getroot() + + # --- Check 1: Root structure --- + check1_ok = True + root_local = etree.QName(root.tag).localname + root_ns = etree.QName(root.tag).namespace or '' + + if root_local != 'MetaDataObject': + r.error(f"1. Root element is '{root_local}', expected 'MetaDataObject'") + r.finalize(out_file) + sys.exit(1) + + if root_ns != EXPECTED_NS: + r.error(f"1. Root namespace is '{root_ns}', expected '{EXPECTED_NS}'") + check1_ok = False + + version = root.get('version', '') + if not version: + r.warn('1. Missing version attribute on MetaDataObject') + elif version not in ('2.17', '2.20'): + r.warn(f"1. Unusual version '{version}' (expected 2.17 or 2.20)") + + # Must have Configuration child + cfg_node = None + for child in root: + if not isinstance(child.tag, str): + continue + if etree.QName(child.tag).localname == 'Configuration' and etree.QName(child.tag).namespace == EXPECTED_NS: + cfg_node = child + break + + if cfg_node is None: + r.error('1. No element found inside MetaDataObject') + r.finalize(out_file) + sys.exit(1) + + # UUID + cfg_uuid = cfg_node.get('uuid', '') + if not cfg_uuid: + r.error('1. Missing uuid on ') + check1_ok = False + elif not GUID_PATTERN.match(cfg_uuid): + r.error(f"1. Invalid uuid '{cfg_uuid}' on ") + check1_ok = False + + # Get name early for header + props_node = cfg_node.find('md:Properties', NS) + name_node = props_node.find('md:Name', NS) if props_node is not None else None + obj_name = (name_node.text or '') if name_node is not None and name_node.text else '(unknown)' + + r.lines.insert(0, f'=== Validation: Extension.{obj_name} ===') + + if check1_ok: + r.ok(f'1. Root structure: MetaDataObject/Configuration, version {version}') + + if r.stopped: + r.finalize(out_file) + sys.exit(1) + + # --- Check 2: InternalInfo --- + internal_info = cfg_node.find('md:InternalInfo', NS) + check2_ok = True + + if internal_info is None: + r.error('2. InternalInfo: missing') + else: + contained = internal_info.findall('xr:ContainedObject', NS) + if len(contained) != 7: + r.warn(f'2. InternalInfo: expected 7 ContainedObject, found {len(contained)}') + + found_class_ids = {} + for co in contained: + class_id_el = co.find('xr:ClassId', NS) + object_id_el = co.find('xr:ObjectId', NS) + + if class_id_el is None or not (class_id_el.text or ''): + r.error('2. ContainedObject missing ClassId') + check2_ok = False + continue + + cid = class_id_el.text + if cid not in VALID_CLASS_IDS: + r.error(f'2. Unknown ClassId: {cid}') + check2_ok = False + + if cid in found_class_ids: + r.error(f'2. Duplicate ClassId: {cid}') + check2_ok = False + found_class_ids[cid] = True + + if object_id_el is None or not (object_id_el.text or ''): + r.error(f'2. ContainedObject missing ObjectId for ClassId {cid}') + check2_ok = False + elif not GUID_PATTERN.match(object_id_el.text): + r.error(f"2. Invalid ObjectId '{object_id_el.text}' for ClassId {cid}") + check2_ok = False + + missing_ids = [cid for cid in VALID_CLASS_IDS if cid not in found_class_ids] + if len(missing_ids) > 0: + r.warn(f'2. Missing ClassIds: {len(missing_ids)} of 7') + + if check2_ok: + r.ok(f'2. InternalInfo: {len(contained)} ContainedObject, all ClassIds valid') + + if r.stopped: + r.finalize(out_file) + sys.exit(1) + + # --- Check 3: Extension-specific properties --- + def_lang = '' + + if props_node is None: + r.error('3. Properties block missing') + else: + check3_ok = True + + # ObjectBelonging = Adopted + ob_node = props_node.find('md:ObjectBelonging', NS) + ob_val = (ob_node.text or '') if ob_node is not None else '' + if ob_val != 'Adopted': + r.error(f"3. ObjectBelonging must be 'Adopted', got '{ob_val}'") + check3_ok = False + + # Name + if name_node is None or not (name_node.text or ''): + r.error('3. Name is missing or empty') + check3_ok = False + else: + name_val = name_node.text + if not IDENT_PATTERN.match(name_val): + r.error(f"3. Name '{name_val}' is not a valid 1C identifier") + check3_ok = False + + # ConfigurationExtensionPurpose + purpose_node = props_node.find('md:ConfigurationExtensionPurpose', NS) + valid_purposes = ['Patch', 'Customization', 'AddOn'] + if purpose_node is None or not (purpose_node.text or ''): + r.error('3. ConfigurationExtensionPurpose is missing') + check3_ok = False + elif purpose_node.text not in valid_purposes: + r.error(f"3. ConfigurationExtensionPurpose '{purpose_node.text}' invalid (expected: Patch, Customization, AddOn)") + check3_ok = False + + # NamePrefix + prefix_node = props_node.find('md:NamePrefix', NS) + if prefix_node is None or not (prefix_node.text or ''): + r.warn('3. NamePrefix is empty') + + # KeepMappingToExtendedConfigurationObjectsByIDs + keep_map_node = props_node.find('md:KeepMappingToExtendedConfigurationObjectsByIDs', NS) + if keep_map_node is None: + r.warn('3. KeepMappingToExtendedConfigurationObjectsByIDs is missing') + + # DefaultLanguage + def_lang_node = props_node.find('md:DefaultLanguage', NS) + def_lang = (def_lang_node.text or '') if def_lang_node is not None else '' + + if check3_ok: + purpose_val = purpose_node.text if purpose_node is not None and purpose_node.text else '?' + prefix_val = (prefix_node.text or '') if prefix_node is not None and prefix_node.text else '(empty)' + r.ok(f'3. Extension properties: Name="{obj_name}", Purpose={purpose_val}, Prefix={prefix_val}') + + if r.stopped: + r.finalize(out_file) + sys.exit(1) + + # --- Check 4: Enum property values --- + if props_node is not None: + enum_checked = 0 + check4_ok = True + + for prop_name, allowed in VALID_ENUM_VALUES.items(): + prop_node = props_node.find(f'md:{prop_name}', NS) + if prop_node is not None and prop_node.text: + val = prop_node.text + if val not in allowed: + r.error(f"4. Property '{prop_name}' has invalid value '{val}'") + check4_ok = False + enum_checked += 1 + + if check4_ok: + r.ok(f'4. Property values: {enum_checked} enum properties checked') + else: + r.warn('4. No Properties block to check') + + if r.stopped: + r.finalize(out_file) + sys.exit(1) + + # --- Check 5: ChildObjects -- valid types, no duplicates, order --- + child_obj_node = cfg_node.find('md:ChildObjects', NS) + + if child_obj_node is None: + r.error('5. ChildObjects block missing') + else: + check5_ok = True + total_count = 0 + type_counts = {} + duplicates = {} + type_first_index = {} + last_type_order = -1 + order_ok = True + + for child in child_obj_node: + if not isinstance(child.tag, str): + continue + type_name = etree.QName(child.tag).localname + obj_name_val = child.text or '' + + if type_name in CHILD_OBJECT_TYPES: + type_idx = CHILD_OBJECT_TYPES.index(type_name) + else: + type_idx = -1 + + if type_idx < 0: + r.error(f"5. Unknown type '{type_name}' in ChildObjects") + check5_ok = False + else: + if type_name not in type_first_index: + type_first_index[type_name] = type_idx + if type_idx < last_type_order: + r.warn(f"5. Type '{type_name}' is out of canonical order (after type at position {last_type_order})") + order_ok = False + last_type_order = type_idx + + if type_name not in type_counts: + type_counts[type_name] = {} + if obj_name_val in type_counts[type_name]: + dup_key = f'{type_name}.{obj_name_val}' + if dup_key not in duplicates: + r.error(f'5. Duplicate: {dup_key}') + duplicates[dup_key] = True + check5_ok = False + else: + type_counts[type_name][obj_name_val] = True + + total_count += 1 + + type_count = len(type_counts) + if check5_ok: + order_info = ', order correct' if order_ok else '' + r.ok(f'5. ChildObjects: {type_count} types, {total_count} objects{order_info}') + + if r.stopped: + r.finalize(out_file) + sys.exit(1) + + # --- Check 6: DefaultLanguage references existing Language in ChildObjects --- + if def_lang and child_obj_node is not None: + lang_name = def_lang + if lang_name.startswith('Language.'): + lang_name = lang_name[9:] + + found = False + for child in child_obj_node: + if not isinstance(child.tag, str): + continue + if etree.QName(child.tag).localname == 'Language' and (child.text or '') == lang_name: + found = True + break + + if found: + r.ok(f'6. DefaultLanguage "{def_lang}" found in ChildObjects') + else: + r.error(f'6. DefaultLanguage "{def_lang}" not found in ChildObjects') + else: + if not def_lang: + r.warn('6. Cannot check DefaultLanguage (empty)') + else: + r.warn('6. Cannot check DefaultLanguage (no ChildObjects)') + + if r.stopped: + r.finalize(out_file) + sys.exit(1) + + # --- Check 7: Language files exist --- + if child_obj_node is not None: + lang_names = [] + for child in child_obj_node: + if not isinstance(child.tag, str): + continue + if etree.QName(child.tag).localname == 'Language': + lang_names.append(child.text or '') + + if len(lang_names) > 0: + exist_count = 0 + for ln in lang_names: + lang_file = os.path.join(config_dir, 'Languages', ln + '.xml') + if os.path.exists(lang_file): + exist_count += 1 + else: + r.warn(f'7. Language file missing: Languages/{ln}.xml') + if exist_count == len(lang_names): + r.ok(f'7. Language files: {exist_count}/{len(lang_names)} exist') + else: + r.warn('7. No Language entries in ChildObjects') + else: + r.warn('7. Cannot check language files (no ChildObjects)') + + if r.stopped: + r.finalize(out_file) + sys.exit(1) + + # --- Check 8: Object directories exist --- + if child_obj_node is not None: + dirs_to_check = {} + for child in child_obj_node: + if not isinstance(child.tag, str): + continue + type_name = etree.QName(child.tag).localname + if type_name == 'Language': + continue + if type_name in CHILD_TYPE_DIR_MAP: + dir_name = CHILD_TYPE_DIR_MAP[type_name] + dirs_to_check[dir_name] = dirs_to_check.get(dir_name, 0) + 1 + + missing_dirs = [] + for dir_name, count in dirs_to_check.items(): + dir_path = os.path.join(config_dir, dir_name) + if not os.path.isdir(dir_path): + missing_dirs.append(f'{dir_name} ({count} objects)') + + if len(missing_dirs) == 0: + r.ok(f'8. Object directories: {len(dirs_to_check)} directories, all exist') + else: + for md in missing_dirs: + r.warn(f'8. Missing directory: {md}') + else: + r.ok('8. Object directories: N/A') + + if r.stopped: + r.finalize(out_file) + sys.exit(1) + + # --- Check 9: Borrowed objects validation --- + if child_obj_node is not None: + borrowed_count = 0 + borrowed_ok_count = 0 + check9_ok = True + + for child in child_obj_node: + if not isinstance(child.tag, str): + continue + type_name = etree.QName(child.tag).localname + child_name = child.text or '' + if type_name == 'Language': + continue + + if type_name not in CHILD_TYPE_DIR_MAP: + continue + dir_name = CHILD_TYPE_DIR_MAP[type_name] + obj_file = os.path.join(config_dir, dir_name, child_name + '.xml') + + if not os.path.exists(obj_file): + continue + + # Parse object XML + obj_doc = None + try: + obj_parser = etree.XMLParser(remove_blank_text=False) + obj_doc = etree.parse(obj_file, obj_parser) + except etree.XMLSyntaxError as e: + r.warn(f'9. Cannot parse {dir_name}/{child_name}.xml: {e}') + continue + + obj_root = obj_doc.getroot() + + # Find the object element (Catalog, Document, etc.) + obj_el = None + for c in obj_root: + if isinstance(c.tag, str): + obj_el = c + break + if obj_el is None: + continue + + obj_props = obj_el.find(f'{{{NS["md"]}}}Properties') + if obj_props is None: + continue + + ob_node = obj_props.find(f'{{{NS["md"]}}}ObjectBelonging') + if ob_node is not None and (ob_node.text or '') == 'Adopted': + borrowed_count += 1 + + # Check ExtendedConfigurationObject + ext_obj = obj_props.find(f'{{{NS["md"]}}}ExtendedConfigurationObject') + if ext_obj is None or not (ext_obj.text or ''): + r.error(f'9. Borrowed {type_name}.{child_name}: missing ExtendedConfigurationObject') + check9_ok = False + elif not GUID_PATTERN.match(ext_obj.text): + r.error(f"9. Borrowed {type_name}.{child_name}: invalid ExtendedConfigurationObject UUID '{ext_obj.text}'") + check9_ok = False + else: + borrowed_ok_count += 1 + + if r.stopped: + break + + if borrowed_count == 0: + r.ok('9. Borrowed objects: none found') + elif check9_ok: + r.ok(f'9. Borrowed objects: {borrowed_ok_count}/{borrowed_count} validated') + + # --- Final output --- + r.finalize(out_file) + sys.exit(1 if r.errors > 0 else 0) + + +if __name__ == '__main__': + main() diff --git a/.claude/skills/db-create/scripts/db-create.py b/.claude/skills/db-create/scripts/db-create.py new file mode 100644 index 00000000..b4a7828e --- /dev/null +++ b/.claude/skills/db-create/scripts/db-create.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python3 +# db-create v1.0 — Create 1C information base +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills + +import argparse +import glob +import os +import random +import shutil +import subprocess +import sys +import tempfile + + +def resolve_v8path(v8path): + """Resolve path to 1cv8.exe.""" + if not v8path: + found = sorted(glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")) + if found: + return found[-1] + else: + print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr) + sys.exit(1) + elif os.path.isdir(v8path): + v8path = os.path.join(v8path, "1cv8.exe") + + if not os.path.isfile(v8path): + print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr) + sys.exit(1) + return v8path + + +def main(): + parser = argparse.ArgumentParser( + description="Create 1C information base", + allow_abbrev=False, + ) + parser.add_argument("-V8Path", default="") + parser.add_argument("-InfoBasePath", default="") + parser.add_argument("-InfoBaseServer", default="") + parser.add_argument("-InfoBaseRef", default="") + parser.add_argument("-UseTemplate", default="") + parser.add_argument("-AddToList", action="store_true") + parser.add_argument("-ListName", default="") + args = parser.parse_args() + + v8path = resolve_v8path(args.V8Path) + + # --- Validate connection --- + if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef): + print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr) + sys.exit(1) + + # --- Validate template --- + if args.UseTemplate and not os.path.exists(args.UseTemplate): + print(f"Error: template file not found: {args.UseTemplate}", file=sys.stderr) + sys.exit(1) + + # --- Temp dir --- + temp_dir = os.path.join(tempfile.gettempdir(), f"db_create_{random.randint(0, 999999)}") + os.makedirs(temp_dir, exist_ok=True) + + try: + # --- Build arguments --- + arguments = ["CREATEINFOBASE"] + + if args.InfoBaseServer and args.InfoBaseRef: + arguments.append(f'Srvr="{args.InfoBaseServer}";Ref="{args.InfoBaseRef}"') + else: + arguments.append(f'File="{args.InfoBasePath}"') + + # --- Template --- + if args.UseTemplate: + arguments.extend(["/UseTemplate", f'"{args.UseTemplate}"']) + + # --- Add to list --- + if args.AddToList: + if args.ListName: + arguments.extend(["/AddToList", f'"{args.ListName}"']) + else: + arguments.append("/AddToList") + + # --- Output --- + out_file = os.path.join(temp_dir, "create_log.txt") + arguments.extend(["/Out", f'"{out_file}"']) + arguments.append("/DisableStartupDialogs") + + # --- Execute --- + print(f"Running: 1cv8.exe {' '.join(arguments)}") + result = subprocess.run( + [v8path] + arguments, + capture_output=True, + text=True, + ) + exit_code = result.returncode + + # --- Result --- + if exit_code == 0: + if args.InfoBaseServer and args.InfoBaseRef: + print(f"Information base created successfully: {args.InfoBaseServer}/{args.InfoBaseRef}") + else: + print(f"Information base created successfully: {args.InfoBasePath}") + else: + print(f"Error creating information base (code: {exit_code})", file=sys.stderr) + + if os.path.isfile(out_file): + try: + with open(out_file, "r", encoding="utf-8-sig") as f: + log_content = f.read() + if log_content: + print("--- Log ---") + print(log_content) + print("--- End ---") + except Exception: + pass + + sys.exit(exit_code) + + finally: + if os.path.isdir(temp_dir): + shutil.rmtree(temp_dir, ignore_errors=True) + + +if __name__ == "__main__": + main() diff --git a/.claude/skills/db-dump-cf/scripts/db-dump-cf.py b/.claude/skills/db-dump-cf/scripts/db-dump-cf.py new file mode 100644 index 00000000..2a4980e0 --- /dev/null +++ b/.claude/skills/db-dump-cf/scripts/db-dump-cf.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python3 +# db-dump-cf v1.0 — Dump 1C configuration to CF file +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills + +import argparse +import glob +import os +import random +import shutil +import subprocess +import sys +import tempfile + + +def resolve_v8path(v8path): + """Resolve path to 1cv8.exe.""" + if not v8path: + found = sorted(glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")) + if found: + return found[-1] + else: + print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr) + sys.exit(1) + elif os.path.isdir(v8path): + v8path = os.path.join(v8path, "1cv8.exe") + + if not os.path.isfile(v8path): + print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr) + sys.exit(1) + return v8path + + +def main(): + parser = argparse.ArgumentParser( + description="Dump 1C configuration to CF file", + allow_abbrev=False, + ) + parser.add_argument("-V8Path", default="") + parser.add_argument("-InfoBasePath", default="") + parser.add_argument("-InfoBaseServer", default="") + parser.add_argument("-InfoBaseRef", default="") + parser.add_argument("-UserName", default="") + parser.add_argument("-Password", default="") + parser.add_argument("-OutputFile", required=True) + parser.add_argument("-Extension", default="") + parser.add_argument("-AllExtensions", action="store_true") + args = parser.parse_args() + + v8path = resolve_v8path(args.V8Path) + + # --- Validate connection --- + if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef): + print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr) + sys.exit(1) + + # --- Ensure output directory exists --- + out_dir = os.path.dirname(args.OutputFile) + if out_dir and not os.path.isdir(out_dir): + os.makedirs(out_dir, exist_ok=True) + + # --- Temp dir --- + temp_dir = os.path.join(tempfile.gettempdir(), f"db_dump_cf_{random.randint(0, 999999)}") + os.makedirs(temp_dir, exist_ok=True) + + try: + # --- Build arguments --- + arguments = ["DESIGNER"] + + if args.InfoBaseServer and args.InfoBaseRef: + arguments.extend(["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"']) + else: + arguments.extend(["/F", f'"{args.InfoBasePath}"']) + + if args.UserName: + arguments.append(f'/N"{args.UserName}"') + if args.Password: + arguments.append(f'/P"{args.Password}"') + + arguments.extend(["/DumpCfg", f'"{args.OutputFile}"']) + + # --- Extensions --- + if args.Extension: + arguments.extend(["-Extension", f'"{args.Extension}"']) + elif args.AllExtensions: + arguments.append("-AllExtensions") + + # --- Output --- + out_file = os.path.join(temp_dir, "dump_cf_log.txt") + arguments.extend(["/Out", f'"{out_file}"']) + arguments.append("/DisableStartupDialogs") + + # --- Execute --- + print(f"Running: 1cv8.exe {' '.join(arguments)}") + result = subprocess.run( + [v8path] + arguments, + capture_output=True, + text=True, + ) + exit_code = result.returncode + + # --- Result --- + if exit_code == 0: + print(f"Configuration dumped successfully to: {args.OutputFile}") + else: + print(f"Error dumping configuration (code: {exit_code})", file=sys.stderr) + + if os.path.isfile(out_file): + try: + with open(out_file, "r", encoding="utf-8-sig") as f: + log_content = f.read() + if log_content: + print("--- Log ---") + print(log_content) + print("--- End ---") + except Exception: + pass + + sys.exit(exit_code) + + finally: + if os.path.isdir(temp_dir): + shutil.rmtree(temp_dir, ignore_errors=True) + + +if __name__ == "__main__": + main() diff --git a/.claude/skills/db-dump-xml/scripts/db-dump-xml.py b/.claude/skills/db-dump-xml/scripts/db-dump-xml.py new file mode 100644 index 00000000..0005b3d6 --- /dev/null +++ b/.claude/skills/db-dump-xml/scripts/db-dump-xml.py @@ -0,0 +1,171 @@ +#!/usr/bin/env python3 +# db-dump-xml v1.0 — Dump 1C configuration to XML files +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills + +import argparse +import glob +import os +import random +import shutil +import subprocess +import sys +import tempfile + + +def resolve_v8path(v8path): + """Resolve path to 1cv8.exe.""" + if not v8path: + candidates = glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe") + if candidates: + candidates.sort() + return candidates[-1] + else: + print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr) + sys.exit(1) + elif os.path.isdir(v8path): + v8path = os.path.join(v8path, "1cv8.exe") + + if not os.path.isfile(v8path): + print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr) + sys.exit(1) + + return v8path + + +def main(): + parser = argparse.ArgumentParser( + description="Dump 1C configuration to XML files", + allow_abbrev=False, + ) + parser.add_argument("-V8Path", default="", help="Path to 1cv8.exe or its bin directory") + parser.add_argument("-InfoBasePath", default="", help="Path to file infobase") + parser.add_argument("-InfoBaseServer", default="", help="1C server (for server infobase)") + parser.add_argument("-InfoBaseRef", default="", help="Infobase name on server") + parser.add_argument("-UserName", default="", help="1C user name") + parser.add_argument("-Password", default="", help="1C user password") + parser.add_argument("-ConfigDir", required=True, help="Directory for configuration dump") + parser.add_argument( + "-Mode", + default="Changes", + choices=["Full", "Changes", "Partial", "UpdateInfo"], + help="Dump mode (default: Changes)", + ) + parser.add_argument("-Objects", default="", help="Comma-separated metadata object names (for Partial mode)") + parser.add_argument("-Extension", default="", help="Extension name to dump") + parser.add_argument("-AllExtensions", action="store_true", help="Dump all extensions") + parser.add_argument( + "-Format", + default="Hierarchical", + choices=["Hierarchical", "Plain"], + help="Dump format (default: Hierarchical)", + ) + args = parser.parse_args() + + # --- Resolve V8Path --- + v8path = resolve_v8path(args.V8Path) + + # --- Validate connection --- + if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef): + print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr) + sys.exit(1) + + # --- Validate Partial mode --- + if args.Mode == "Partial" and not args.Objects: + print("Error: -Objects required for Partial mode", file=sys.stderr) + sys.exit(1) + + # --- Create output dir if needed --- + if not os.path.exists(args.ConfigDir): + os.makedirs(args.ConfigDir, exist_ok=True) + print(f"Created output directory: {args.ConfigDir}") + + # --- Temp dir --- + temp_dir = os.path.join(tempfile.gettempdir(), f"db_dump_xml_{random.randint(0, 999999)}") + os.makedirs(temp_dir, exist_ok=True) + + try: + # --- Build arguments --- + arguments = ["DESIGNER"] + + if args.InfoBaseServer and args.InfoBaseRef: + arguments += ["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"'] + else: + arguments += ["/F", f'"{args.InfoBasePath}"'] + + if args.UserName: + arguments.append(f'/N"{args.UserName}"') + if args.Password: + arguments.append(f'/P"{args.Password}"') + + arguments += ["/DumpConfigToFiles", f'"{args.ConfigDir}"'] + arguments += ["-Format", args.Format] + + if args.Mode == "Full": + print("Executing full configuration dump...") + elif args.Mode == "Changes": + print("Executing incremental configuration dump...") + arguments.append("-update") + arguments.append("-force") + elif args.Mode == "Partial": + print("Executing partial configuration dump...") + object_list = [obj.strip() for obj in args.Objects.split(",") if obj.strip()] + + list_file = os.path.join(temp_dir, "dump_list.txt") + with open(list_file, "w", encoding="utf-8-sig") as f: + f.write("\n".join(object_list)) + + arguments += ["-listFile", f'"{list_file}"'] + print(f"Objects to dump: {len(object_list)}") + for obj in object_list: + print(f" {obj}") + elif args.Mode == "UpdateInfo": + print("Updating ConfigDumpInfo.xml...") + arguments.append("-configDumpInfoOnly") + + # --- Extensions --- + if args.Extension: + arguments += ["-Extension", f'"{args.Extension}"'] + elif args.AllExtensions: + arguments.append("-AllExtensions") + + # --- Output --- + out_file = os.path.join(temp_dir, "dump_log.txt") + arguments += ["/Out", f'"{out_file}"'] + arguments.append("/DisableStartupDialogs") + + # --- Execute --- + print(f"Running: 1cv8.exe {' '.join(arguments)}") + result = subprocess.run( + [v8path] + arguments, + capture_output=True, + text=True, + ) + exit_code = result.returncode + + # --- Result --- + if exit_code == 0: + print("Dump completed successfully") + print(f"Configuration dumped to: {args.ConfigDir}") + else: + print(f"Error dumping configuration (code: {exit_code})", file=sys.stderr) + + if os.path.isfile(out_file): + try: + with open(out_file, "r", encoding="utf-8-sig") as f: + log_content = f.read() + if log_content: + print("--- Log ---") + print(log_content) + print("--- End ---") + except Exception: + pass + + sys.exit(exit_code) + + finally: + if os.path.exists(temp_dir): + shutil.rmtree(temp_dir, ignore_errors=True) + + +if __name__ == "__main__": + main() diff --git a/.claude/skills/db-load-cf/scripts/db-load-cf.py b/.claude/skills/db-load-cf/scripts/db-load-cf.py new file mode 100644 index 00000000..0c4ecf9c --- /dev/null +++ b/.claude/skills/db-load-cf/scripts/db-load-cf.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python3 +# db-load-cf v1.0 — Load 1C configuration from CF file +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills + +import argparse +import glob +import os +import random +import shutil +import subprocess +import sys +import tempfile + + +def resolve_v8path(v8path): + """Resolve path to 1cv8.exe.""" + if not v8path: + found = sorted(glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")) + if found: + return found[-1] + else: + print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr) + sys.exit(1) + elif os.path.isdir(v8path): + v8path = os.path.join(v8path, "1cv8.exe") + + if not os.path.isfile(v8path): + print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr) + sys.exit(1) + return v8path + + +def main(): + parser = argparse.ArgumentParser( + description="Load 1C configuration from CF file", + allow_abbrev=False, + ) + parser.add_argument("-V8Path", default="") + parser.add_argument("-InfoBasePath", default="") + parser.add_argument("-InfoBaseServer", default="") + parser.add_argument("-InfoBaseRef", default="") + parser.add_argument("-UserName", default="") + parser.add_argument("-Password", default="") + parser.add_argument("-InputFile", required=True) + parser.add_argument("-Extension", default="") + parser.add_argument("-AllExtensions", action="store_true") + args = parser.parse_args() + + v8path = resolve_v8path(args.V8Path) + + # --- Validate connection --- + if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef): + print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr) + sys.exit(1) + + # --- Validate input file --- + if not os.path.isfile(args.InputFile): + print(f"Error: input file not found: {args.InputFile}", file=sys.stderr) + sys.exit(1) + + # --- Temp dir --- + temp_dir = os.path.join(tempfile.gettempdir(), f"db_load_cf_{random.randint(0, 999999)}") + os.makedirs(temp_dir, exist_ok=True) + + try: + # --- Build arguments --- + arguments = ["DESIGNER"] + + if args.InfoBaseServer and args.InfoBaseRef: + arguments.extend(["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"']) + else: + arguments.extend(["/F", f'"{args.InfoBasePath}"']) + + if args.UserName: + arguments.append(f'/N"{args.UserName}"') + if args.Password: + arguments.append(f'/P"{args.Password}"') + + arguments.extend(["/LoadCfg", f'"{args.InputFile}"']) + + # --- Extensions --- + if args.Extension: + arguments.extend(["-Extension", f'"{args.Extension}"']) + elif args.AllExtensions: + arguments.append("-AllExtensions") + + # --- Output --- + out_file = os.path.join(temp_dir, "load_cf_log.txt") + arguments.extend(["/Out", f'"{out_file}"']) + arguments.append("/DisableStartupDialogs") + + # --- Execute --- + print(f"Running: 1cv8.exe {' '.join(arguments)}") + result = subprocess.run( + [v8path] + arguments, + capture_output=True, + text=True, + ) + exit_code = result.returncode + + # --- Result --- + if exit_code == 0: + print(f"Configuration loaded successfully from: {args.InputFile}") + else: + print(f"Error loading configuration (code: {exit_code})", file=sys.stderr) + + if os.path.isfile(out_file): + try: + with open(out_file, "r", encoding="utf-8-sig") as f: + log_content = f.read() + if log_content: + print("--- Log ---") + print(log_content) + print("--- End ---") + except Exception: + pass + + sys.exit(exit_code) + + finally: + if os.path.isdir(temp_dir): + shutil.rmtree(temp_dir, ignore_errors=True) + + +if __name__ == "__main__": + main() diff --git a/.claude/skills/db-load-git/scripts/db-load-git.py b/.claude/skills/db-load-git/scripts/db-load-git.py new file mode 100644 index 00000000..f7b8317b --- /dev/null +++ b/.claude/skills/db-load-git/scripts/db-load-git.py @@ -0,0 +1,282 @@ +#!/usr/bin/env python3 +# db-load-git v1.0 — Load Git changes into 1C database +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills + +import argparse +import glob +import os +import random +import re +import shutil +import subprocess +import sys +import tempfile + + +def resolve_v8path(v8path): + """Resolve path to 1cv8.exe.""" + if not v8path: + candidates = glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe") + if candidates: + candidates.sort() + return candidates[-1] + else: + print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr) + sys.exit(1) + elif os.path.isdir(v8path): + v8path = os.path.join(v8path, "1cv8.exe") + + if not os.path.isfile(v8path): + print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr) + sys.exit(1) + + return v8path + + +def get_object_xml_from_bsl(relative_path): + """Map BSL path to object XML path.""" + parts = re.split(r"[\\/]", relative_path) + if len(parts) >= 2: + return f"{parts[0]}/{parts[1]}.xml" + return None + + +def run_git(config_dir, git_args): + """Run a git command in config_dir and return output lines on success.""" + result = subprocess.run( + ["git"] + git_args, + capture_output=True, + text=True, + cwd=config_dir, + ) + if result.returncode == 0: + return [line for line in result.stdout.splitlines() if line.strip()] + return [] + + +def main(): + parser = argparse.ArgumentParser( + description="Load Git changes into 1C database", + allow_abbrev=False, + ) + parser.add_argument("-V8Path", default="", help="Path to 1cv8.exe or its bin directory") + parser.add_argument("-InfoBasePath", default="", help="Path to file infobase") + parser.add_argument("-InfoBaseServer", default="", help="1C server (for server infobase)") + parser.add_argument("-InfoBaseRef", default="", help="Infobase name on server") + parser.add_argument("-UserName", default="", help="1C user name") + parser.add_argument("-Password", default="", help="1C user password") + parser.add_argument("-ConfigDir", required=True, help="Directory with XML configuration (git repo)") + parser.add_argument( + "-Source", + default="All", + choices=["All", "Staged", "Unstaged", "Commit"], + help="Change source (default: All)", + ) + parser.add_argument("-CommitRange", default="", help="Commit range (for Source=Commit), e.g. HEAD~3..HEAD") + parser.add_argument("-Extension", default="", help="Extension name to load") + parser.add_argument("-AllExtensions", action="store_true", help="Load all extensions") + parser.add_argument( + "-Format", + default="Hierarchical", + choices=["Hierarchical", "Plain"], + help="File format (default: Hierarchical)", + ) + parser.add_argument("-DryRun", action="store_true", help="Only show what would be loaded (no actual load)") + args = parser.parse_args() + + # --- Resolve V8Path (skip if DryRun) --- + v8path = None + if not args.DryRun: + v8path = resolve_v8path(args.V8Path) + + # --- Validate connection (skip if DryRun) --- + if not args.DryRun: + if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef): + print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr) + sys.exit(1) + + # --- Validate config dir --- + if not os.path.exists(args.ConfigDir): + print(f"Error: config directory not found: {args.ConfigDir}", file=sys.stderr) + sys.exit(1) + + # --- Validate Commit mode --- + if args.Source == "Commit" and not args.CommitRange: + print("Error: -CommitRange required for Source=Commit", file=sys.stderr) + sys.exit(1) + + # --- Check git --- + try: + subprocess.run(["git", "--version"], capture_output=True, text=True, check=True) + except (subprocess.CalledProcessError, FileNotFoundError): + print("Error: git not found in PATH", file=sys.stderr) + sys.exit(1) + + # --- Get changed files from Git --- + changed_files = [] + + if args.Source == "Staged": + print("Getting staged changes...") + changed_files += run_git(args.ConfigDir, ["diff", "--cached", "--name-only"]) + elif args.Source == "Unstaged": + print("Getting unstaged changes...") + changed_files += run_git(args.ConfigDir, ["diff", "--name-only"]) + changed_files += run_git(args.ConfigDir, ["ls-files", "--others", "--exclude-standard"]) + elif args.Source == "Commit": + print(f"Getting changes from {args.CommitRange}...") + changed_files += run_git(args.ConfigDir, ["diff", "--name-only", args.CommitRange]) + elif args.Source == "All": + print("Getting all uncommitted changes...") + changed_files += run_git(args.ConfigDir, ["diff", "--cached", "--name-only"]) + changed_files += run_git(args.ConfigDir, ["diff", "--name-only"]) + changed_files += run_git(args.ConfigDir, ["ls-files", "--others", "--exclude-standard"]) + + # Deduplicate and filter blanks + changed_files = list(dict.fromkeys(f for f in changed_files if f.strip())) + + if len(changed_files) == 0: + print("No changes found") + sys.exit(0) + + print(f"Git changes detected: {len(changed_files)} files") + + # --- Filter and map to config files --- + config_files = [] + + for file in changed_files: + file = file.strip().replace("\\", "/") + if not file: + continue + + # Skip service files + if file == "ConfigDumpInfo.xml": + continue + + # Only process .xml and .bsl files + if not re.search(r"\.(xml|bsl)$", file): + continue + + # Check file exists in config dir + full_path = os.path.join(args.ConfigDir, file) + + if file.endswith(".xml"): + if os.path.exists(full_path): + if file not in config_files: + config_files.append(file) + elif file.endswith(".bsl"): + # For BSL: add the BSL itself + parent object XML + all Ext/ files + object_xml = get_object_xml_from_bsl(file) + if object_xml: + full_xml_path = os.path.join(args.ConfigDir, object_xml) + if os.path.exists(full_xml_path): + if object_xml not in config_files: + config_files.append(object_xml) + if file not in config_files: + config_files.append(file) + + # Add all files from Ext/ directory of the object + parts = re.split(r"[\\/]", file) + if len(parts) >= 2: + ext_dir = os.path.join(args.ConfigDir, parts[0], parts[1], "Ext") + if os.path.isdir(ext_dir): + for root, dirs, files in os.walk(ext_dir): + for fname in files: + abs_path = os.path.join(root, fname) + # Build relative path from ConfigDir + rel_path = os.path.relpath(abs_path, args.ConfigDir).replace("\\", "/") + if rel_path not in config_files: + config_files.append(rel_path) + + if len(config_files) == 0: + print("No configuration files found in changes") + sys.exit(0) + + print(f"Files for loading: {len(config_files)}") + for f in config_files: + print(f" {f}") + + # --- DryRun: stop here --- + if args.DryRun: + print("") + print("DryRun mode - no changes applied") + sys.exit(0) + + # --- Temp dir --- + temp_dir = os.path.join(tempfile.gettempdir(), f"db_load_git_{random.randint(0, 999999)}") + os.makedirs(temp_dir, exist_ok=True) + + try: + # --- Write list file (UTF-8 with BOM) --- + list_file = os.path.join(temp_dir, "load_list.txt") + with open(list_file, "w", encoding="utf-8-sig") as f: + f.write("\n".join(config_files)) + + # --- Build arguments --- + arguments = ["DESIGNER"] + + if args.InfoBaseServer and args.InfoBaseRef: + arguments += ["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"'] + else: + arguments += ["/F", f'"{args.InfoBasePath}"'] + + if args.UserName: + arguments.append(f'/N"{args.UserName}"') + if args.Password: + arguments.append(f'/P"{args.Password}"') + + arguments += ["/LoadConfigFromFiles", f'"{args.ConfigDir}"'] + arguments += ["-listFile", f'"{list_file}"'] + arguments += ["-Format", args.Format] + arguments.append("-partial") + arguments.append("-updateConfigDumpInfo") + + # --- Extensions --- + if args.Extension: + arguments += ["-Extension", f'"{args.Extension}"'] + elif args.AllExtensions: + arguments.append("-AllExtensions") + + # --- Output --- + out_file = os.path.join(temp_dir, "load_log.txt") + arguments += ["/Out", f'"{out_file}"'] + arguments.append("/DisableStartupDialogs") + + # --- Execute --- + print("") + print("Executing partial configuration load...") + print(f"Running: 1cv8.exe {' '.join(arguments)}") + + result = subprocess.run( + [v8path] + arguments, + capture_output=True, + text=True, + ) + exit_code = result.returncode + + # --- Result --- + print("") + if exit_code == 0: + print("Load completed successfully") + else: + print(f"Error loading configuration (code: {exit_code})", file=sys.stderr) + + if os.path.isfile(out_file): + try: + with open(out_file, "r", encoding="utf-8-sig") as f: + log_content = f.read() + if log_content: + print("--- Log ---") + print(log_content) + print("--- End ---") + except Exception: + pass + + sys.exit(exit_code) + + finally: + if os.path.exists(temp_dir): + shutil.rmtree(temp_dir, ignore_errors=True) + + +if __name__ == "__main__": + main() diff --git a/.claude/skills/db-load-xml/scripts/db-load-xml.py b/.claude/skills/db-load-xml/scripts/db-load-xml.py new file mode 100644 index 00000000..9a2968d5 --- /dev/null +++ b/.claude/skills/db-load-xml/scripts/db-load-xml.py @@ -0,0 +1,178 @@ +#!/usr/bin/env python3 +# db-load-xml v1.0 — Load 1C configuration from XML files +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills + +import argparse +import glob +import os +import random +import shutil +import subprocess +import sys +import tempfile + + +def resolve_v8path(v8path): + """Resolve path to 1cv8.exe.""" + if not v8path: + candidates = glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe") + if candidates: + candidates.sort() + return candidates[-1] + else: + print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr) + sys.exit(1) + elif os.path.isdir(v8path): + v8path = os.path.join(v8path, "1cv8.exe") + + if not os.path.isfile(v8path): + print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr) + sys.exit(1) + + return v8path + + +def main(): + parser = argparse.ArgumentParser( + description="Load 1C configuration from XML files", + allow_abbrev=False, + ) + parser.add_argument("-V8Path", default="", help="Path to 1cv8.exe or its bin directory") + parser.add_argument("-InfoBasePath", default="", help="Path to file infobase") + parser.add_argument("-InfoBaseServer", default="", help="1C server (for server infobase)") + parser.add_argument("-InfoBaseRef", default="", help="Infobase name on server") + parser.add_argument("-UserName", default="", help="1C user name") + parser.add_argument("-Password", default="", help="1C user password") + parser.add_argument("-ConfigDir", required=True, help="Directory with XML configuration sources") + parser.add_argument( + "-Mode", + default="Full", + choices=["Full", "Partial"], + help="Load mode (default: Full)", + ) + parser.add_argument("-Files", default="", help="Comma-separated relative file paths (for Partial mode)") + parser.add_argument("-ListFile", default="", help="Path to file list (alternative to -Files, for Partial mode)") + parser.add_argument("-Extension", default="", help="Extension name to load") + parser.add_argument("-AllExtensions", action="store_true", help="Load all extensions") + parser.add_argument( + "-Format", + default="Hierarchical", + choices=["Hierarchical", "Plain"], + help="File format (default: Hierarchical)", + ) + args = parser.parse_args() + + # --- Resolve V8Path --- + v8path = resolve_v8path(args.V8Path) + + # --- Validate connection --- + if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef): + print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr) + sys.exit(1) + + # --- Validate config dir --- + if not os.path.exists(args.ConfigDir): + print(f"Error: config directory not found: {args.ConfigDir}", file=sys.stderr) + sys.exit(1) + + # --- Validate Partial mode --- + if args.Mode == "Partial" and not args.Files and not args.ListFile: + print("Error: -Files or -ListFile required for Partial mode", file=sys.stderr) + sys.exit(1) + + # --- Temp dir --- + temp_dir = os.path.join(tempfile.gettempdir(), f"db_load_xml_{random.randint(0, 999999)}") + os.makedirs(temp_dir, exist_ok=True) + + try: + # --- Build arguments --- + arguments = ["DESIGNER"] + + if args.InfoBaseServer and args.InfoBaseRef: + arguments += ["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"'] + else: + arguments += ["/F", f'"{args.InfoBasePath}"'] + + if args.UserName: + arguments.append(f'/N"{args.UserName}"') + if args.Password: + arguments.append(f'/P"{args.Password}"') + + arguments += ["/LoadConfigFromFiles", f'"{args.ConfigDir}"'] + + if args.Mode == "Full": + print("Executing full configuration load...") + else: + print("Executing partial configuration load...") + + # Build list file + generated_list_file = None + if args.ListFile: + # Use provided list file + if not os.path.isfile(args.ListFile): + print(f"Error: list file not found: {args.ListFile}", file=sys.stderr) + sys.exit(1) + generated_list_file = args.ListFile + else: + # Generate from -Files parameter + file_list = [f.strip() for f in args.Files.split(",") if f.strip()] + generated_list_file = os.path.join(temp_dir, "load_list.txt") + with open(generated_list_file, "w", encoding="utf-8-sig") as f: + f.write("\n".join(file_list)) + + print(f"Files to load: {len(file_list)}") + for fl in file_list: + print(f" {fl}") + + arguments += ["-listFile", f'"{generated_list_file}"'] + arguments.append("-partial") + arguments.append("-updateConfigDumpInfo") + + arguments += ["-Format", args.Format] + + # --- Extensions --- + if args.Extension: + arguments += ["-Extension", f'"{args.Extension}"'] + elif args.AllExtensions: + arguments.append("-AllExtensions") + + # --- Output --- + out_file = os.path.join(temp_dir, "load_log.txt") + arguments += ["/Out", f'"{out_file}"'] + arguments.append("/DisableStartupDialogs") + + # --- Execute --- + print(f"Running: 1cv8.exe {' '.join(arguments)}") + result = subprocess.run( + [v8path] + arguments, + capture_output=True, + text=True, + ) + exit_code = result.returncode + + # --- Result --- + if exit_code == 0: + print("Load completed successfully") + else: + print(f"Error loading configuration (code: {exit_code})", file=sys.stderr) + + if os.path.isfile(out_file): + try: + with open(out_file, "r", encoding="utf-8-sig") as f: + log_content = f.read() + if log_content: + print("--- Log ---") + print(log_content) + print("--- End ---") + except Exception: + pass + + sys.exit(exit_code) + + finally: + if os.path.exists(temp_dir): + shutil.rmtree(temp_dir, ignore_errors=True) + + +if __name__ == "__main__": + main() diff --git a/.claude/skills/db-run/scripts/db-run.py b/.claude/skills/db-run/scripts/db-run.py new file mode 100644 index 00000000..0c2a3cc1 --- /dev/null +++ b/.claude/skills/db-run/scripts/db-run.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 +# db-run v1.0 — Launch 1C:Enterprise +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills + +import argparse +import glob +import os +import subprocess +import sys + + +def resolve_v8path(v8path): + """Resolve path to 1cv8.exe.""" + if not v8path: + found = sorted(glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")) + if found: + return found[-1] + else: + print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr) + sys.exit(1) + elif os.path.isdir(v8path): + v8path = os.path.join(v8path, "1cv8.exe") + + if not os.path.isfile(v8path): + print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr) + sys.exit(1) + return v8path + + +def main(): + parser = argparse.ArgumentParser( + description="Launch 1C:Enterprise", + allow_abbrev=False, + ) + parser.add_argument("-V8Path", default="") + parser.add_argument("-InfoBasePath", default="") + parser.add_argument("-InfoBaseServer", default="") + parser.add_argument("-InfoBaseRef", default="") + parser.add_argument("-UserName", default="") + parser.add_argument("-Password", default="") + parser.add_argument("-Execute", default="") + parser.add_argument("-CParam", default="") + parser.add_argument("-URL", default="") + args = parser.parse_args() + + v8path = resolve_v8path(args.V8Path) + + # --- Validate connection --- + if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef): + print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr) + sys.exit(1) + + # --- Build arguments --- + arguments = ["ENTERPRISE"] + + if args.InfoBaseServer and args.InfoBaseRef: + arguments.extend(["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"']) + else: + arguments.extend(["/F", f'"{args.InfoBasePath}"']) + + if args.UserName: + arguments.append(f'/N"{args.UserName}"') + if args.Password: + arguments.append(f'/P"{args.Password}"') + + # --- Optional params --- + execute = args.Execute + if execute: + ext = os.path.splitext(execute)[1].lower() + if ext == ".erf": + print("[WARN] /Execute does not support ERF files (external reports).") + print(f" Open the report via File -> Open: {execute}") + print(" Launching database without /Execute.") + execute = "" + + if execute: + arguments.extend(["/Execute", f'"{execute}"']) + if args.CParam: + arguments.extend(["/C", f'"{args.CParam}"']) + if args.URL: + arguments.extend(["/URL", f'"{args.URL}"']) + + arguments.append("/DisableStartupDialogs") + + # --- Execute (background, no wait) --- + print(f"Running: 1cv8.exe {' '.join(arguments)}") + subprocess.Popen([v8path] + arguments) + print("1C:Enterprise launched") + + +if __name__ == "__main__": + main() diff --git a/.claude/skills/db-update/scripts/db-update.py b/.claude/skills/db-update/scripts/db-update.py new file mode 100644 index 00000000..16c1c886 --- /dev/null +++ b/.claude/skills/db-update/scripts/db-update.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python3 +# db-update v1.0 — Update 1C database configuration +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills + +import argparse +import glob +import os +import random +import shutil +import subprocess +import sys +import tempfile + + +def resolve_v8path(v8path): + """Resolve path to 1cv8.exe.""" + if not v8path: + found = sorted(glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")) + if found: + return found[-1] + else: + print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr) + sys.exit(1) + elif os.path.isdir(v8path): + v8path = os.path.join(v8path, "1cv8.exe") + + if not os.path.isfile(v8path): + print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr) + sys.exit(1) + return v8path + + +def main(): + parser = argparse.ArgumentParser( + description="Update 1C database configuration", + allow_abbrev=False, + ) + parser.add_argument("-V8Path", default="") + parser.add_argument("-InfoBasePath", default="") + parser.add_argument("-InfoBaseServer", default="") + parser.add_argument("-InfoBaseRef", default="") + parser.add_argument("-UserName", default="") + parser.add_argument("-Password", default="") + parser.add_argument("-Extension", default="") + parser.add_argument("-AllExtensions", action="store_true") + parser.add_argument("-Dynamic", default="", choices=["", "+", "-"]) + parser.add_argument("-Server", action="store_true") + parser.add_argument("-WarningsAsErrors", action="store_true") + args = parser.parse_args() + + v8path = resolve_v8path(args.V8Path) + + # --- Validate connection --- + if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef): + print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr) + sys.exit(1) + + # --- Temp dir --- + temp_dir = os.path.join(tempfile.gettempdir(), f"db_update_{random.randint(0, 999999)}") + os.makedirs(temp_dir, exist_ok=True) + + try: + # --- Build arguments --- + arguments = ["DESIGNER"] + + if args.InfoBaseServer and args.InfoBaseRef: + arguments.extend(["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"']) + else: + arguments.extend(["/F", f'"{args.InfoBasePath}"']) + + if args.UserName: + arguments.append(f'/N"{args.UserName}"') + if args.Password: + arguments.append(f'/P"{args.Password}"') + + arguments.append("/UpdateDBCfg") + + # --- Options --- + if args.Dynamic: + arguments.append(f"-Dynamic{args.Dynamic}") + if args.Server: + arguments.append("-Server") + if args.WarningsAsErrors: + arguments.append("-WarningsAsErrors") + + # --- Extensions --- + if args.Extension: + arguments.extend(["-Extension", f'"{args.Extension}"']) + elif args.AllExtensions: + arguments.append("-AllExtensions") + + # --- Output --- + out_file = os.path.join(temp_dir, "update_log.txt") + arguments.extend(["/Out", f'"{out_file}"']) + arguments.append("/DisableStartupDialogs") + + # --- Execute --- + print(f"Running: 1cv8.exe {' '.join(arguments)}") + result = subprocess.run( + [v8path] + arguments, + capture_output=True, + text=True, + ) + exit_code = result.returncode + + # --- Result --- + if exit_code == 0: + print("Database configuration updated successfully") + else: + print(f"Error updating database configuration (code: {exit_code})", file=sys.stderr) + + if os.path.isfile(out_file): + try: + with open(out_file, "r", encoding="utf-8-sig") as f: + log_content = f.read() + if log_content: + print("--- Log ---") + print(log_content) + print("--- End ---") + except Exception: + pass + + sys.exit(exit_code) + + finally: + if os.path.isdir(temp_dir): + shutil.rmtree(temp_dir, ignore_errors=True) + + +if __name__ == "__main__": + main() diff --git a/.claude/skills/epf-add-form/scripts/add-form.py b/.claude/skills/epf-add-form/scripts/add-form.py new file mode 100644 index 00000000..816dd2cb --- /dev/null +++ b/.claude/skills/epf-add-form/scripts/add-form.py @@ -0,0 +1,249 @@ +#!/usr/bin/env python3 +# add-form v1.0 — Add managed form to 1C external data processor +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills + +import argparse +import os +import sys +import uuid + +from lxml import etree + +NSMAP = {"md": "http://v8.1c.ru/8.3/MDClasses"} + + +def save_xml_with_bom(tree, path): + """Save XML tree to file with UTF-8 BOM.""" + xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8") + xml_bytes = xml_bytes.replace(b"encoding='UTF-8'", b'encoding="UTF-8"') + with open(path, "wb") as f: + f.write(b"\xef\xbb\xbf") + f.write(xml_bytes) + + +def write_text_with_bom(path, text): + """Write text to file with UTF-8 BOM.""" + with open(path, "w", encoding="utf-8-sig") as f: + f.write(text) + + +def main(): + parser = argparse.ArgumentParser(description="Add managed form to 1C processor", allow_abbrev=False) + parser.add_argument("-ProcessorName", required=True) + parser.add_argument("-FormName", required=True) + parser.add_argument("-Synonym", default=None) + parser.add_argument("-Main", action="store_true") + parser.add_argument("-SrcDir", default="src") + args = parser.parse_args() + + processor_name = args.ProcessorName + form_name = args.FormName + synonym = args.Synonym if args.Synonym is not None else form_name + is_main = args.Main + src_dir = args.SrcDir + + # --- Checks --- + + root_xml_path = os.path.join(src_dir, f"{processor_name}.xml") + if not os.path.exists(root_xml_path): + print(f"Корневой файл обработки не найден: {root_xml_path}. Сначала выполните epf-init.", file=sys.stderr) + sys.exit(1) + + processor_dir = os.path.join(src_dir, processor_name) + forms_dir = os.path.join(processor_dir, "Forms") + form_meta_path = os.path.join(forms_dir, f"{form_name}.xml") + + if os.path.exists(form_meta_path): + print(f"Форма уже существует: {form_meta_path}", file=sys.stderr) + sys.exit(1) + + # --- Create directories --- + + form_dir = os.path.join(forms_dir, form_name) + form_ext_dir = os.path.join(form_dir, "Ext") + form_module_dir = os.path.join(form_ext_dir, "Form") + + os.makedirs(form_module_dir, exist_ok=True) + + # --- 1. Form metadata (Forms/.xml) --- + + form_uuid = str(uuid.uuid4()) + + form_meta_xml = ( + '\n' + '\n' + f'\t
\n' + '\t\t\n' + f'\t\t\t{form_name}\n' + '\t\t\t\n' + '\t\t\t\t\n' + '\t\t\t\t\tru\n' + f'\t\t\t\t\t{synonym}\n' + '\t\t\t\t\n' + '\t\t\t\n' + '\t\t\t\n' + '\t\t\tManaged\n' + '\t\t\tfalse\n' + '\t\t\t\n' + '\t\t\t\tPlatformApplication\n' + '\t\t\t\tMobilePlatformApplication\n' + '\t\t\t\n' + '\t\t\t\n' + '\t\t\n' + '\t
\n' + '
' + ) + + write_text_with_bom(form_meta_path, form_meta_xml) + + # --- 2. Form description (Forms//Ext/Form.xml) --- + + form_xml_path = os.path.join(form_ext_dir, "Form.xml") + + form_xml = ( + '\n' + '
\n' + '\t\n' + '\t\ttrue\n' + '\t\n' + '\t\n' + '\t\n' + f'\t\t\n' + '\t\t\t\n' + f'\t\t\t\tcfg:ExternalDataProcessorObject.{processor_name}\n' + '\t\t\t\n' + '\t\t\ttrue\n' + '\t\t\n' + '\t\n' + '' + ) + + write_text_with_bom(form_xml_path, form_xml) + + # --- 3. BSL module (Forms//Ext/Form/Module.bsl) --- + + module_path = os.path.join(form_module_dir, "Module.bsl") + + module_bsl = ( + '#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u0421\u043e\u0431\u044b\u0442\u0438\u0439\u0424\u043e\u0440\u043c\u044b\n' + '\n' + '#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438\n' + '\n' + '#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u0421\u043e\u0431\u044b\u0442\u0438\u0439\u042d\u043b\u0435\u043c\u0435\u043d\u0442\u043e\u0432\u0424\u043e\u0440\u043c\u044b\n' + '\n' + '#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438\n' + '\n' + '#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u041a\u043e\u043c\u0430\u043d\u0434\u0424\u043e\u0440\u043c\u044b\n' + '\n' + '#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438\n' + '\n' + '#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u041e\u043f\u043e\u0432\u0435\u0449\u0435\u043d\u0438\u0439\n' + '\n' + '#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438\n' + '\n' + '#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u0421\u043b\u0443\u0436\u0435\u0431\u043d\u044b\u0435\u041f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u044b\u0418\u0424\u0443\u043d\u043a\u0446\u0438\u0438\n' + '\n' + '#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438' + ) + + write_text_with_bom(module_path, module_bsl) + + # --- 4. Modify root XML --- + + root_xml_full = os.path.abspath(root_xml_path) + parser_xml = etree.XMLParser(remove_blank_text=False) + tree = etree.parse(root_xml_full, parser_xml) + root = tree.getroot() + + ns = "http://v8.1c.ru/8.3/MDClasses" + child_objects = root.find(".//md:ChildObjects", NSMAP) + if child_objects is None: + print(f"Не найден элемент ChildObjects в {root_xml_path}", file=sys.stderr) + sys.exit(1) + + # Add
before first