#!/usr/bin/env python3 # cf-edit v1.4 — 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 import uuid as _uuid 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", ] # Type → on-disk directory name (plural) TYPE_TO_DIR = { "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", } 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"", b'') if not xml_bytes.endswith(b"\n"): xml_bytes += b"\n" with open(path, "wb") as f: f.write(b"\xef\xbb\xbf") f.write(xml_bytes) def main(): sys.stdout.reconfigure(encoding="utf-8") sys.stderr.reconfigure(encoding="utf-8") parser = argparse.ArgumentParser(description="Edit 1C configuration root (Configuration.xml)", allow_abbrev=False) parser.add_argument("-ConfigPath", "-Path", 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", "set-panels", "set-home-page"]) 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) config_dir = os.path.dirname(resolved_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) # Check that the referenced object actually exists on disk. # cf-edit add-childObject is a low-level operation for rare scenarios # (e.g. restoring a rolled-back Configuration.xml when object files are intact). # For creating NEW objects, meta-compile/role-compile/subsystem-compile already # auto-register in Configuration.xml — calling cf-edit add-childObject there is # unnecessary and error-prone. type_dir = TYPE_TO_DIR.get(type_name) obj_file = os.path.join(config_dir, type_dir, f"{obj_name_val}.xml") if not os.path.exists(obj_file): hint_skill = {"Subsystem": "subsystem-compile", "Role": "role-compile"}.get(type_name, "meta-compile") print( f"Object file not found: {type_dir}/{obj_name_val}.xml\n" f"cf-edit add-childObject only references objects that already exist on disk.\n" f"To create a new {type_name}, use {hint_skill} (auto-registers in Configuration.xml):\n" f' /{hint_skill} with {{"type":"{type_name}","name":"{obj_name_val}"}}', file=sys.stderr ) sys.exit(1) # 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") # --- set-panels (writes Ext/ClientApplicationInterface.xml from scratch) --- # Canonical English aliases — preferred form, used in docs and error messages. PANEL_UUIDS = { "sections": "b553047f-c9aa-4157-978d-448ecad24248", "open": "cbab57f2-a0f3-4f0a-89ea-4cb19570ab75", "favorites": "13322b22-3960-4d68-93a6-fe2dd7f28ca3", "history": "c933ac92-92cd-459d-81cc-e0c8a83ced99", "functions": "b2735bd3-d822-4430-ba59-c9e869693b24", } # Russian synonyms — silently accepted (cf-info displays Russian names; # users may copy them straight into cf-edit value). PANEL_SYNONYMS = { "разделов": "sections", "разделы": "sections", "открытых": "open", "открытые": "open", "избранного": "favorites","избранное": "favorites", "истории": "history", "история": "history", "функций": "functions", "функции": "functions", } def build_panel_entry_xml(entry, indent): if isinstance(entry, str): key = entry.lower() key = PANEL_SYNONYMS.get(key, key) if key not in PANEL_UUIDS: allowed = ", ".join(sorted(PANEL_UUIDS.keys())) print(f"Unknown panel alias '{entry}'. Allowed: {allowed}", file=sys.stderr) sys.exit(1) inst = str(_uuid.uuid4()) return f'{indent}\r\n{indent}\t{PANEL_UUIDS[key]}\r\n{indent}' if isinstance(entry, dict) and "group" in entry: children = entry["group"] if not children: print("group must contain at least one entry", file=sys.stderr) sys.exit(1) gid = str(_uuid.uuid4()) inner = "" for child in children: child_xml = build_panel_entry_xml(child, indent + "\t\t") inner += f"{indent}\t\r\n{child_xml}\r\n{indent}\t\r\n" return f'{indent}\r\n{inner}{indent}' print(f"Panel entry must be string alias or {{group:[...]}}, got: {entry!r}", file=sys.stderr) sys.exit(1) def do_set_panels(value): nonlocal modify_count layout = value if isinstance(layout, str): try: layout = json.loads(layout) except json.JSONDecodeError: print(f"set-panels value must be valid JSON object", file=sys.stderr) sys.exit(1) if not isinstance(layout, dict) or not layout: print("set-panels value must be non-empty object", file=sys.stderr) sys.exit(1) sides = ("top", "left", "right", "bottom") # Reject unknown side keys for k in layout.keys(): if k not in sides: print(f"Unknown side '{k}'. Allowed: {', '.join(sides)}", file=sys.stderr) sys.exit(1) body_parts = [] for side in sides: entries = layout.get(side) if entries is None: continue if not isinstance(entries, list): entries = [entries] for entry in entries: entry_xml = build_panel_entry_xml(entry, "\t\t") body_parts.append(f"\t<{side}>\r\n{entry_xml}\r\n\t") body = "\r\n".join(body_parts) body_block = body + "\r\n" if body else "" declarations = ( '\t\r\n' '\t\r\n' '\t\r\n' '\t\r\n' '\t' ) cai_xml = ( '\r\n' '\r\n' f'{body_block}{declarations}\r\n' '' ) ext_dir = os.path.join(config_dir, "Ext") os.makedirs(ext_dir, exist_ok=True) cai_path = os.path.join(ext_dir, "ClientApplicationInterface.xml") with open(cai_path, "w", encoding="utf-8-sig", newline="") as fh: fh.write(cai_xml) modify_count += 1 info(f"Wrote panel layout: {cai_path}") # --- set-home-page (writes Ext/HomePageWorkArea.xml from scratch) --- RU_TYPE_MAP = { "справочник": "Catalog", "документ": "Document", "перечисление": "Enum", "отчёт": "Report", "отчет": "Report", "обработка": "DataProcessor", "общаяформа": "CommonForm", "журналдокументов": "DocumentJournal", "планвидовхарактеристик": "ChartOfCharacteristicTypes", "плансчетов": "ChartOfAccounts", "планвидоврасчета": "ChartOfCalculationTypes", "планвидоврасчёта": "ChartOfCalculationTypes", "регистрсведений": "InformationRegister", "регистрнакопления": "AccumulationRegister", "регистрбухгалтерии": "AccountingRegister", "регистррасчета": "CalculationRegister", "регистррасчёта": "CalculationRegister", "бизнеспроцесс": "BusinessProcess", "задача": "Task", "планобмена": "ExchangePlan", "хранилищенастроек": "SettingsStorage", } DIR_TO_TYPE = {v.lower(): k for k, v in TYPE_TO_DIR.items()} UUID_RE = __import__("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}$") def normalize_form_ref(s): s = (s or "").strip() if not s: return s if UUID_RE.match(s): return s if "/" in s or "\\" in s: parts = [p for p in s.replace("\\", "/").split("/") if p and p.lower() != "ext"] if parts and parts[-1].lower() == "form.xml": parts = parts[:-1] if len(parts) >= 2: type_dir = parts[0] type_singular = DIR_TO_TYPE.get(type_dir.lower()) if type_singular: if type_singular == "CommonForm" and len(parts) >= 2: return f"CommonForm.{parts[1]}" if len(parts) >= 4 and parts[2].lower() == "forms": return f"{type_singular}.{parts[1]}.Form.{parts[3]}" return s segs = s.split(".") if segs: head = segs[0].lower() if head in RU_TYPE_MAP: segs[0] = RU_TYPE_MAP[head] for i in range(1, len(segs)): if segs[i] == "Форма": segs[i] = "Form" if len(segs) == 3 and segs[0] in TYPE_ORDER and segs[0] != "CommonForm": segs = [segs[0], segs[1], "Form", segs[2]] return ".".join(segs) def get_field(obj, keys): for k in keys: if isinstance(obj, dict) and k in obj: return obj[k] return None def build_home_page_item_xml(entry, indent): if isinstance(entry, str): form_ref = normalize_form_ref(entry) height = 10 common = True roles = None elif isinstance(entry, dict): form_raw = get_field(entry, ["form", "Form"]) if not form_raw: print(f"Home page item: 'form' is required, got: {entry!r}", file=sys.stderr) sys.exit(1) form_ref = normalize_form_ref(str(form_raw)) h = get_field(entry, ["height", "Height"]) height = int(h) if h is not None else 10 vis = get_field(entry, ["visibility", "Visibility"]) common = bool(vis) if vis is not None else True roles = get_field(entry, ["roles"]) else: print(f"Home page item must be string or object, got: {entry!r}", file=sys.stderr) sys.exit(1) vis_parts = [f"{indent}\t\t{str(common).lower()}"] if roles and isinstance(roles, dict): for rname, rval in roles.items(): if not rname.startswith("Role.") and not UUID_RE.match(rname): rname = f"Role.{rname}" rval_s = str(bool(rval)).lower() vis_parts.append(f'{indent}\t\t{rval_s}') vis_block = "\r\n".join(vis_parts) esc_form = html_escape(form_ref, quote=True) return ( f"{indent}\r\n" f"{indent}\t
{esc_form}
\r\n" f"{indent}\t{height}\r\n" f"{indent}\t\r\n" f"{vis_block}\r\n" f"{indent}\t\r\n" f"{indent}
" ) def do_set_home_page(value): nonlocal modify_count layout = value if isinstance(layout, str): try: layout = json.loads(layout) except json.JSONDecodeError: print("set-home-page value must be valid JSON object", file=sys.stderr) sys.exit(1) if not isinstance(layout, dict) or not layout: print("set-home-page value must be non-empty object", file=sys.stderr) sys.exit(1) allowed_templates = ("OneColumn", "TwoColumnsEqualWidth", "TwoColumnsVariableWidth") tmpl = get_field(layout, ["template", "WorkingAreaTemplate"]) or "TwoColumnsEqualWidth" if tmpl not in allowed_templates: print(f"Unknown template '{tmpl}'. Allowed: {', '.join(allowed_templates)}", file=sys.stderr) sys.exit(1) left_items = get_field(layout, ["left", "LeftColumn"]) right_items = get_field(layout, ["right", "RightColumn"]) known = {"template", "WorkingAreaTemplate", "left", "LeftColumn", "right", "RightColumn"} for k in layout.keys(): if k not in known: print(f"Unknown key '{k}'. Allowed: template, left, right", file=sys.stderr) sys.exit(1) if tmpl == "OneColumn" and right_items: print("Template 'OneColumn' cannot have items in 'right' column", file=sys.stderr) sys.exit(1) def build_column(tag, items): if not items: return f"\t<{tag}/>" if not isinstance(items, list): items = [items] if not items: return f"\t<{tag}/>" blocks = [build_home_page_item_xml(it, "\t\t") for it in items] body = "\r\n".join(blocks) return f"\t<{tag}>\r\n{body}\r\n\t" left_xml = build_column("LeftColumn", left_items) right_xml = build_column("RightColumn", right_items) hp_xml = ( '\r\n' '\r\n' f'\t{tmpl}\r\n' f'{left_xml}\r\n' f'{right_xml}\r\n' '' ) ext_dir = os.path.join(config_dir, "Ext") os.makedirs(ext_dir, exist_ok=True) hp_path = os.path.join(ext_dir, "HomePageWorkArea.xml") with open(hp_path, "w", encoding="utf-8-sig", newline="") as fh: fh.write(hp_xml) modify_count += 1 info(f"Wrote home page layout: {hp_path}") # --- 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 if isinstance(op_value, str) else str(op_value)) elif op_name == "add-childObject": do_add_child_object(op_value if isinstance(op_value, str) else str(op_value)) elif op_name == "remove-childObject": do_remove_child_object(op_value if isinstance(op_value, str) else str(op_value)) elif op_name == "add-defaultRole": do_add_default_role(op_value if isinstance(op_value, str) else str(op_value)) elif op_name == "remove-defaultRole": do_remove_default_role(op_value if isinstance(op_value, str) else str(op_value)) elif op_name == "set-defaultRoles": do_set_default_roles(op_value if isinstance(op_value, str) else str(op_value)) elif op_name == "set-panels": do_set_panels(op_value) elif op_name == "set-home-page": do_set_home_page(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", "-Path", 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()