From 7cc96ae0ae3316a68f289f2ead0313daee3fcbf3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 4 Jun 2026 09:28:02 +0000 Subject: [PATCH] Auto-build: codex (powershell) from 6d119eb --- .codex-plugin/plugin.json | 36 + .codex/skills/.gitignore | 1 + .codex/skills/cf-edit/SKILL.md | 60 + .codex/skills/cf-edit/reference.md | 150 + .codex/skills/cf-edit/scripts/cf-edit.ps1 | 869 ++++ .codex/skills/cf-edit/scripts/cf-edit.py | 822 ++++ .codex/skills/cf-info/SKILL.md | 54 + .codex/skills/cf-info/scripts/cf-info.ps1 | 579 +++ .codex/skills/cf-info/scripts/cf-info.py | 562 +++ .codex/skills/cf-init/SKILL.md | 49 + .codex/skills/cf-init/scripts/cf-init.ps1 | 249 ++ .codex/skills/cf-init/scripts/cf-init.py | 233 + .codex/skills/cf-validate/SKILL.md | 29 + .../cf-validate/scripts/cf-validate.ps1 | 611 +++ .../skills/cf-validate/scripts/cf-validate.py | 600 +++ .codex/skills/cfe-borrow/SKILL.md | 101 + .../skills/cfe-borrow/scripts/cfe-borrow.ps1 | 1772 ++++++++ .../skills/cfe-borrow/scripts/cfe-borrow.py | 1559 +++++++ .codex/skills/cfe-diff/SKILL.md | 57 + .codex/skills/cfe-diff/scripts/cfe-diff.ps1 | 471 ++ .codex/skills/cfe-diff/scripts/cfe-diff.py | 540 +++ .codex/skills/cfe-init/SKILL.md | 71 + .codex/skills/cfe-init/scripts/cfe-init.ps1 | 270 ++ .codex/skills/cfe-init/scripts/cfe-init.py | 248 ++ .codex/skills/cfe-patch-method/SKILL.md | 78 + .../scripts/cfe-patch-method.ps1 | 209 + .../scripts/cfe-patch-method.py | 247 ++ .codex/skills/cfe-validate/SKILL.md | 29 + .../cfe-validate/scripts/cfe-validate.ps1 | 939 ++++ .../cfe-validate/scripts/cfe-validate.py | 894 ++++ .codex/skills/db-create/SKILL.md | 78 + .codex/skills/db-create/scripts/db-create.ps1 | 163 + .codex/skills/db-create/scripts/db-create.py | 127 + .codex/skills/db-dump-cf/SKILL.md | 79 + .../skills/db-dump-cf/scripts/db-dump-cf.ps1 | 166 + .../skills/db-dump-cf/scripts/db-dump-cf.py | 128 + .codex/skills/db-dump-xml/SKILL.md | 97 + .../db-dump-xml/scripts/db-dump-xml.ps1 | 224 + .../skills/db-dump-xml/scripts/db-dump-xml.py | 173 + .codex/skills/db-list/SKILL.md | 158 + .codex/skills/db-load-cf/SKILL.md | 81 + .../skills/db-load-cf/scripts/db-load-cf.ps1 | 166 + .../skills/db-load-cf/scripts/db-load-cf.py | 128 + .codex/skills/db-load-git/SKILL.md | 78 + .../db-load-git/scripts/db-load-git.ps1 | 359 ++ .../skills/db-load-git/scripts/db-load-git.py | 285 ++ .codex/skills/db-load-xml/SKILL.md | 109 + .../db-load-xml/scripts/db-load-xml.ps1 | 279 ++ .../skills/db-load-xml/scripts/db-load-xml.py | 228 + .codex/skills/db-run/SKILL.md | 76 + .codex/skills/db-run/scripts/db-run.ps1 | 145 + .codex/skills/db-run/scripts/db-run.py | 94 + .codex/skills/db-update/SKILL.md | 93 + .codex/skills/db-update/scripts/db-update.ps1 | 184 + .codex/skills/db-update/scripts/db-update.py | 133 + .codex/skills/epf-bsp-add-command/SKILL.md | 196 + .codex/skills/epf-bsp-init/SKILL.md | 208 + .codex/skills/epf-build/SKILL.md | 69 + .codex/skills/epf-build/scripts/epf-build.ps1 | 173 + .codex/skills/epf-build/scripts/epf-build.py | 143 + .../epf-build/scripts/stub-db-create.ps1 | 1295 ++++++ .../epf-build/scripts/stub-db-create.py | 1085 +++++ .codex/skills/epf-dump/SKILL.md | 69 + .codex/skills/epf-dump/scripts/epf-dump.ps1 | 167 + .codex/skills/epf-dump/scripts/epf-dump.py | 136 + .codex/skills/epf-init/SKILL.md | 41 + .codex/skills/epf-init/scripts/init.ps1 | 90 + .codex/skills/epf-init/scripts/init.py | 99 + .codex/skills/epf-validate/SKILL.md | 30 + .../epf-validate/scripts/epf-validate.ps1 | 842 ++++ .../epf-validate/scripts/epf-validate.py | 708 +++ .codex/skills/erf-build/SKILL.md | 71 + .codex/skills/erf-dump/SKILL.md | 71 + .codex/skills/erf-init/SKILL.md | 42 + .codex/skills/erf-init/scripts/init.ps1 | 180 + .codex/skills/erf-init/scripts/init.py | 167 + .codex/skills/erf-validate/SKILL.md | 32 + .codex/skills/form-add/SKILL.md | 71 + .codex/skills/form-add/scripts/form-add.ps1 | 478 ++ .codex/skills/form-add/scripts/form-add.py | 472 ++ .codex/skills/form-compile/SKILL.md | 550 +++ .codex/skills/form-compile/presets/README.md | 126 + .../form-compile/presets/erp-standard.json | 68 + .../form-compile/scripts/form-compile.ps1 | 3389 ++++++++++++++ .../form-compile/scripts/form-compile.py | 3148 +++++++++++++ .codex/skills/form-edit/SKILL.md | 142 + .codex/skills/form-edit/scripts/form-edit.ps1 | 1260 ++++++ .codex/skills/form-edit/scripts/form-edit.py | 1321 ++++++ .codex/skills/form-info/SKILL.md | 30 + .codex/skills/form-info/scripts/form-info.ps1 | 664 +++ .codex/skills/form-info/scripts/form-info.py | 684 +++ .codex/skills/form-patterns/SKILL.md | 253 ++ .codex/skills/form-remove/SKILL.md | 47 + .../form-remove/scripts/remove-form.ps1 | 89 + .../skills/form-remove/scripts/remove-form.py | 101 + .codex/skills/form-validate/SKILL.md | 29 + .../form-validate/scripts/form-validate.ps1 | 825 ++++ .../form-validate/scripts/form-validate.py | 730 +++ .codex/skills/help-add/SKILL.md | 44 + .codex/skills/help-add/scripts/add-help.ps1 | 138 + .codex/skills/help-add/scripts/add-help.py | 166 + .codex/skills/img-grid/SKILL.md | 77 + .../skills/img-grid/scripts/overlay-grid.py | 115 + .codex/skills/interface-edit/SKILL.md | 75 + .../interface-edit/scripts/interface-edit.ps1 | 562 +++ .../interface-edit/scripts/interface-edit.py | 519 +++ .codex/skills/interface-validate/SKILL.md | 29 + .../scripts/interface-validate.ps1 | 403 ++ .../scripts/interface-validate.py | 400 ++ .codex/skills/meta-compile/SKILL.md | 119 + .../meta-compile/reference/types-basic.md | 116 + .../meta-compile/reference/types-process.md | 136 + .../meta-compile/reference/types-registers.md | 174 + .../meta-compile/reference/types-web.md | 103 + .../meta-compile/scripts/meta-compile.ps1 | 3125 +++++++++++++ .../meta-compile/scripts/meta-compile.py | 2741 ++++++++++++ .codex/skills/meta-edit/SKILL.md | 108 + .codex/skills/meta-edit/child-operations.md | 116 + .codex/skills/meta-edit/json-dsl.md | 148 + .../skills/meta-edit/properties-reference.md | 54 + .codex/skills/meta-edit/scripts/meta-edit.ps1 | 2418 ++++++++++ .codex/skills/meta-edit/scripts/meta-edit.py | 2283 ++++++++++ .codex/skills/meta-info/SKILL.md | 87 + .codex/skills/meta-info/scripts/meta-info.ps1 | 1153 +++++ .codex/skills/meta-info/scripts/meta-info.py | 1132 +++++ .codex/skills/meta-remove/SKILL.md | 60 + .../meta-remove/scripts/meta-remove.ps1 | 495 +++ .../skills/meta-remove/scripts/meta-remove.py | 485 ++ .codex/skills/meta-validate/SKILL.md | 29 + .../meta-validate/scripts/meta-validate.ps1 | 1340 ++++++ .../meta-validate/scripts/meta-validate.py | 1247 ++++++ .codex/skills/mxl-compile/SKILL.md | 65 + .../mxl-compile/scripts/mxl-compile.ps1 | 733 +++ .../skills/mxl-compile/scripts/mxl-compile.py | 636 +++ .codex/skills/mxl-decompile/SKILL.md | 57 + .../mxl-decompile/scripts/mxl-decompile.ps1 | 646 +++ .../mxl-decompile/scripts/mxl-decompile.py | 705 +++ .codex/skills/mxl-info/SKILL.md | 132 + .codex/skills/mxl-info/scripts/mxl-info.ps1 | 484 ++ .codex/skills/mxl-info/scripts/mxl-info.py | 445 ++ .codex/skills/mxl-validate/SKILL.md | 29 + .../mxl-validate/scripts/mxl-validate.ps1 | 423 ++ .../mxl-validate/scripts/mxl-validate.py | 389 ++ .codex/skills/role-compile/SKILL.md | 109 + .codex/skills/role-compile/dsl-reference.md | 313 ++ .../role-compile/scripts/role-compile.ps1 | 748 ++++ .../role-compile/scripts/role-compile.py | 656 +++ .codex/skills/role-info/SKILL.md | 44 + .codex/skills/role-info/scripts/role-info.ps1 | 245 + .codex/skills/role-info/scripts/role-info.py | 232 + .codex/skills/role-validate/SKILL.md | 27 + .../role-validate/scripts/role-validate.ps1 | 510 +++ .../role-validate/scripts/role-validate.py | 512 +++ .codex/skills/skd-compile/SKILL.md | 436 ++ .../skd-compile/examples/skd-styles.json | 30 + .../skd-compile/scripts/skd-compile.ps1 | 3566 +++++++++++++++ .../skills/skd-compile/scripts/skd-compile.py | 2906 ++++++++++++ .codex/skills/skd-decompile/SKILL.md | 52 + .../skd-decompile/scripts/skd-decompile.ps1 | 3036 +++++++++++++ .../skd-decompile/scripts/skd-decompile.py | 3149 +++++++++++++ .codex/skills/skd-edit/SKILL.md | 373 ++ .codex/skills/skd-edit/scripts/skd-edit.ps1 | 3929 +++++++++++++++++ .codex/skills/skd-edit/scripts/skd-edit.py | 3266 ++++++++++++++ .codex/skills/skd-info/SKILL.md | 80 + .codex/skills/skd-info/scripts/skd-info.ps1 | 1912 ++++++++ .codex/skills/skd-info/scripts/skd-info.py | 1742 ++++++++ .codex/skills/skd-validate/SKILL.md | 29 + .../skd-validate/scripts/skd-validate.ps1 | 935 ++++ .../skd-validate/scripts/skd-validate.py | 872 ++++ .codex/skills/subsystem-compile/SKILL.md | 59 + .../scripts/subsystem-compile.ps1 | 553 +++ .../scripts/subsystem-compile.py | 450 ++ .codex/skills/subsystem-edit/SKILL.md | 57 + .../subsystem-edit/scripts/subsystem-edit.ps1 | 546 +++ .../subsystem-edit/scripts/subsystem-edit.py | 618 +++ .codex/skills/subsystem-info/SKILL.md | 62 + .../subsystem-info/scripts/subsystem-info.ps1 | 514 +++ .../subsystem-info/scripts/subsystem-info.py | 525 +++ .codex/skills/subsystem-validate/SKILL.md | 29 + .../scripts/subsystem-validate.ps1 | 352 ++ .../scripts/subsystem-validate.py | 371 ++ .codex/skills/template-add/SKILL.md | 89 + .../template-add/scripts/add-template.ps1 | 266 ++ .../template-add/scripts/add-template.py | 295 ++ .codex/skills/template-remove/SKILL.md | 47 + .../scripts/remove-template.ps1 | 90 + .../scripts/remove-template.py | 102 + .codex/skills/web-info/SKILL.md | 62 + .codex/skills/web-info/scripts/web-info.ps1 | 148 + .codex/skills/web-info/scripts/web-info.py | 160 + .codex/skills/web-publish/SKILL.md | 101 + .../web-publish/scripts/web-publish.ps1 | 398 ++ .../skills/web-publish/scripts/web-publish.py | 428 ++ .codex/skills/web-stop/SKILL.md | 52 + .codex/skills/web-stop/scripts/web-stop.ps1 | 92 + .codex/skills/web-stop/scripts/web-stop.py | 119 + .codex/skills/web-test/.gitignore | 4 + .codex/skills/web-test/SKILL.md | 579 +++ .codex/skills/web-test/recording.md | 348 ++ .codex/skills/web-test/regress.md | 424 ++ .codex/skills/web-test/scripts/browser.mjs | 56 + .../web-test/scripts/cli/commands/exec.mjs | 36 + .../web-test/scripts/cli/commands/run.mjs | 22 + .../web-test/scripts/cli/commands/shot.mjs | 18 + .../web-test/scripts/cli/commands/start.mjs | 33 + .../web-test/scripts/cli/commands/status.mjs | 14 + .../web-test/scripts/cli/commands/stop.mjs | 17 + .../web-test/scripts/cli/commands/test.mjs | 458 ++ .../web-test/scripts/cli/exec-context.mjs | 148 + .codex/skills/web-test/scripts/cli/server.mjs | 37 + .../skills/web-test/scripts/cli/session.mjs | 20 + .../scripts/cli/test-runner/assertions.mjs | 64 + .../scripts/cli/test-runner/discover.mjs | 43 + .../scripts/cli/test-runner/reporters.mjs | 113 + .../scripts/cli/test-runner/severity.mjs | 66 + .codex/skills/web-test/scripts/cli/util.mjs | 113 + .codex/skills/web-test/scripts/dom.mjs | 94 + .../skills/web-test/scripts/dom/_shared.mjs | 391 ++ .codex/skills/web-test/scripts/dom/edd.mjs | 108 + .../web-test/scripts/dom/edit-state.mjs | 63 + .../web-test/scripts/dom/errors-stack.mjs | 65 + .codex/skills/web-test/scripts/dom/errors.mjs | 127 + .codex/skills/web-test/scripts/dom/filter.mjs | 187 + .../web-test/scripts/dom/form-state.mjs | 34 + .codex/skills/web-test/scripts/dom/forms.mjs | 647 +++ .../skills/web-test/scripts/dom/grid-edit.mjs | 292 ++ .codex/skills/web-test/scripts/dom/grid.mjs | 755 ++++ .codex/skills/web-test/scripts/dom/nav.mjs | 93 + .../skills/web-test/scripts/dom/submenu.mjs | 149 + .../web-test/scripts/engine/core/click.mjs | 129 + .../scripts/engine/core/clipboard.mjs | 97 + .../web-test/scripts/engine/core/errors.mjs | 310 ++ .../web-test/scripts/engine/core/helpers.mjs | 178 + .../scripts/engine/core/scroll-horiz.mjs | 47 + .../web-test/scripts/engine/core/session.mjs | 404 ++ .../web-test/scripts/engine/core/state.mjs | 113 + .../web-test/scripts/engine/core/wait.mjs | 123 + .../scripts/engine/forms/click-form.mjs | 122 + .../scripts/engine/forms/click-popup.mjs | 90 + .../web-test/scripts/engine/forms/close.mjs | 56 + .../web-test/scripts/engine/forms/fill.mjs | 147 + .../scripts/engine/forms/select-value.mjs | 849 ++++ .../web-test/scripts/engine/forms/state.mjs | 32 + .../scripts/engine/nav/navigation.mjs | 253 ++ .../scripts/engine/recording/captions.mjs | 292 ++ .../scripts/engine/recording/capture.mjs | 243 + .../scripts/engine/recording/highlight.mjs | 340 ++ .../scripts/engine/recording/narration.mjs | 196 + .../web-test/scripts/engine/recording/tts.mjs | 175 + .../engine/spreadsheet/spreadsheet.mjs | 561 +++ .../scripts/engine/table/click-cell.mjs | 235 + .../scripts/engine/table/click-row.mjs | 95 + .../web-test/scripts/engine/table/filter.mjs | 248 ++ .../scripts/engine/table/grid-toggle.mjs | 64 + .../web-test/scripts/engine/table/grid.mjs | 95 + .../scripts/engine/table/row-fill.mjs | 957 ++++ .../skills/web-test/scripts/package-lock.json | 59 + .codex/skills/web-test/scripts/package.json | 10 + .codex/skills/web-test/scripts/run.mjs | 65 + .codex/skills/web-unpublish/SKILL.md | 62 + .../web-unpublish/scripts/web-unpublish.ps1 | 159 + .../web-unpublish/scripts/web-unpublish.py | 162 + LICENSE | 21 + README.md | 27 + 264 files changed, 110480 insertions(+) create mode 100644 .codex-plugin/plugin.json create mode 100644 .codex/skills/.gitignore create mode 100644 .codex/skills/cf-edit/SKILL.md create mode 100644 .codex/skills/cf-edit/reference.md create mode 100644 .codex/skills/cf-edit/scripts/cf-edit.ps1 create mode 100644 .codex/skills/cf-edit/scripts/cf-edit.py create mode 100644 .codex/skills/cf-info/SKILL.md create mode 100644 .codex/skills/cf-info/scripts/cf-info.ps1 create mode 100644 .codex/skills/cf-info/scripts/cf-info.py create mode 100644 .codex/skills/cf-init/SKILL.md create mode 100644 .codex/skills/cf-init/scripts/cf-init.ps1 create mode 100644 .codex/skills/cf-init/scripts/cf-init.py create mode 100644 .codex/skills/cf-validate/SKILL.md create mode 100644 .codex/skills/cf-validate/scripts/cf-validate.ps1 create mode 100644 .codex/skills/cf-validate/scripts/cf-validate.py create mode 100644 .codex/skills/cfe-borrow/SKILL.md create mode 100644 .codex/skills/cfe-borrow/scripts/cfe-borrow.ps1 create mode 100644 .codex/skills/cfe-borrow/scripts/cfe-borrow.py create mode 100644 .codex/skills/cfe-diff/SKILL.md create mode 100644 .codex/skills/cfe-diff/scripts/cfe-diff.ps1 create mode 100644 .codex/skills/cfe-diff/scripts/cfe-diff.py create mode 100644 .codex/skills/cfe-init/SKILL.md create mode 100644 .codex/skills/cfe-init/scripts/cfe-init.ps1 create mode 100644 .codex/skills/cfe-init/scripts/cfe-init.py create mode 100644 .codex/skills/cfe-patch-method/SKILL.md create mode 100644 .codex/skills/cfe-patch-method/scripts/cfe-patch-method.ps1 create mode 100644 .codex/skills/cfe-patch-method/scripts/cfe-patch-method.py create mode 100644 .codex/skills/cfe-validate/SKILL.md create mode 100644 .codex/skills/cfe-validate/scripts/cfe-validate.ps1 create mode 100644 .codex/skills/cfe-validate/scripts/cfe-validate.py create mode 100644 .codex/skills/db-create/SKILL.md create mode 100644 .codex/skills/db-create/scripts/db-create.ps1 create mode 100644 .codex/skills/db-create/scripts/db-create.py create mode 100644 .codex/skills/db-dump-cf/SKILL.md create mode 100644 .codex/skills/db-dump-cf/scripts/db-dump-cf.ps1 create mode 100644 .codex/skills/db-dump-cf/scripts/db-dump-cf.py create mode 100644 .codex/skills/db-dump-xml/SKILL.md create mode 100644 .codex/skills/db-dump-xml/scripts/db-dump-xml.ps1 create mode 100644 .codex/skills/db-dump-xml/scripts/db-dump-xml.py create mode 100644 .codex/skills/db-list/SKILL.md create mode 100644 .codex/skills/db-load-cf/SKILL.md create mode 100644 .codex/skills/db-load-cf/scripts/db-load-cf.ps1 create mode 100644 .codex/skills/db-load-cf/scripts/db-load-cf.py create mode 100644 .codex/skills/db-load-git/SKILL.md create mode 100644 .codex/skills/db-load-git/scripts/db-load-git.ps1 create mode 100644 .codex/skills/db-load-git/scripts/db-load-git.py create mode 100644 .codex/skills/db-load-xml/SKILL.md create mode 100644 .codex/skills/db-load-xml/scripts/db-load-xml.ps1 create mode 100644 .codex/skills/db-load-xml/scripts/db-load-xml.py create mode 100644 .codex/skills/db-run/SKILL.md create mode 100644 .codex/skills/db-run/scripts/db-run.ps1 create mode 100644 .codex/skills/db-run/scripts/db-run.py create mode 100644 .codex/skills/db-update/SKILL.md create mode 100644 .codex/skills/db-update/scripts/db-update.ps1 create mode 100644 .codex/skills/db-update/scripts/db-update.py create mode 100644 .codex/skills/epf-bsp-add-command/SKILL.md create mode 100644 .codex/skills/epf-bsp-init/SKILL.md create mode 100644 .codex/skills/epf-build/SKILL.md create mode 100644 .codex/skills/epf-build/scripts/epf-build.ps1 create mode 100644 .codex/skills/epf-build/scripts/epf-build.py create mode 100644 .codex/skills/epf-build/scripts/stub-db-create.ps1 create mode 100644 .codex/skills/epf-build/scripts/stub-db-create.py create mode 100644 .codex/skills/epf-dump/SKILL.md create mode 100644 .codex/skills/epf-dump/scripts/epf-dump.ps1 create mode 100644 .codex/skills/epf-dump/scripts/epf-dump.py create mode 100644 .codex/skills/epf-init/SKILL.md create mode 100644 .codex/skills/epf-init/scripts/init.ps1 create mode 100644 .codex/skills/epf-init/scripts/init.py create mode 100644 .codex/skills/epf-validate/SKILL.md create mode 100644 .codex/skills/epf-validate/scripts/epf-validate.ps1 create mode 100644 .codex/skills/epf-validate/scripts/epf-validate.py create mode 100644 .codex/skills/erf-build/SKILL.md create mode 100644 .codex/skills/erf-dump/SKILL.md create mode 100644 .codex/skills/erf-init/SKILL.md create mode 100644 .codex/skills/erf-init/scripts/init.ps1 create mode 100644 .codex/skills/erf-init/scripts/init.py create mode 100644 .codex/skills/erf-validate/SKILL.md create mode 100644 .codex/skills/form-add/SKILL.md create mode 100644 .codex/skills/form-add/scripts/form-add.ps1 create mode 100644 .codex/skills/form-add/scripts/form-add.py create mode 100644 .codex/skills/form-compile/SKILL.md create mode 100644 .codex/skills/form-compile/presets/README.md create mode 100644 .codex/skills/form-compile/presets/erp-standard.json create mode 100644 .codex/skills/form-compile/scripts/form-compile.ps1 create mode 100644 .codex/skills/form-compile/scripts/form-compile.py create mode 100644 .codex/skills/form-edit/SKILL.md create mode 100644 .codex/skills/form-edit/scripts/form-edit.ps1 create mode 100644 .codex/skills/form-edit/scripts/form-edit.py create mode 100644 .codex/skills/form-info/SKILL.md create mode 100644 .codex/skills/form-info/scripts/form-info.ps1 create mode 100644 .codex/skills/form-info/scripts/form-info.py create mode 100644 .codex/skills/form-patterns/SKILL.md create mode 100644 .codex/skills/form-remove/SKILL.md create mode 100644 .codex/skills/form-remove/scripts/remove-form.ps1 create mode 100644 .codex/skills/form-remove/scripts/remove-form.py create mode 100644 .codex/skills/form-validate/SKILL.md create mode 100644 .codex/skills/form-validate/scripts/form-validate.ps1 create mode 100644 .codex/skills/form-validate/scripts/form-validate.py create mode 100644 .codex/skills/help-add/SKILL.md create mode 100644 .codex/skills/help-add/scripts/add-help.ps1 create mode 100644 .codex/skills/help-add/scripts/add-help.py create mode 100644 .codex/skills/img-grid/SKILL.md create mode 100644 .codex/skills/img-grid/scripts/overlay-grid.py create mode 100644 .codex/skills/interface-edit/SKILL.md create mode 100644 .codex/skills/interface-edit/scripts/interface-edit.ps1 create mode 100644 .codex/skills/interface-edit/scripts/interface-edit.py create mode 100644 .codex/skills/interface-validate/SKILL.md create mode 100644 .codex/skills/interface-validate/scripts/interface-validate.ps1 create mode 100644 .codex/skills/interface-validate/scripts/interface-validate.py create mode 100644 .codex/skills/meta-compile/SKILL.md create mode 100644 .codex/skills/meta-compile/reference/types-basic.md create mode 100644 .codex/skills/meta-compile/reference/types-process.md create mode 100644 .codex/skills/meta-compile/reference/types-registers.md create mode 100644 .codex/skills/meta-compile/reference/types-web.md create mode 100644 .codex/skills/meta-compile/scripts/meta-compile.ps1 create mode 100644 .codex/skills/meta-compile/scripts/meta-compile.py create mode 100644 .codex/skills/meta-edit/SKILL.md create mode 100644 .codex/skills/meta-edit/child-operations.md create mode 100644 .codex/skills/meta-edit/json-dsl.md create mode 100644 .codex/skills/meta-edit/properties-reference.md create mode 100644 .codex/skills/meta-edit/scripts/meta-edit.ps1 create mode 100644 .codex/skills/meta-edit/scripts/meta-edit.py create mode 100644 .codex/skills/meta-info/SKILL.md create mode 100644 .codex/skills/meta-info/scripts/meta-info.ps1 create mode 100644 .codex/skills/meta-info/scripts/meta-info.py create mode 100644 .codex/skills/meta-remove/SKILL.md create mode 100644 .codex/skills/meta-remove/scripts/meta-remove.ps1 create mode 100644 .codex/skills/meta-remove/scripts/meta-remove.py create mode 100644 .codex/skills/meta-validate/SKILL.md create mode 100644 .codex/skills/meta-validate/scripts/meta-validate.ps1 create mode 100644 .codex/skills/meta-validate/scripts/meta-validate.py create mode 100644 .codex/skills/mxl-compile/SKILL.md create mode 100644 .codex/skills/mxl-compile/scripts/mxl-compile.ps1 create mode 100644 .codex/skills/mxl-compile/scripts/mxl-compile.py create mode 100644 .codex/skills/mxl-decompile/SKILL.md create mode 100644 .codex/skills/mxl-decompile/scripts/mxl-decompile.ps1 create mode 100644 .codex/skills/mxl-decompile/scripts/mxl-decompile.py create mode 100644 .codex/skills/mxl-info/SKILL.md create mode 100644 .codex/skills/mxl-info/scripts/mxl-info.ps1 create mode 100644 .codex/skills/mxl-info/scripts/mxl-info.py create mode 100644 .codex/skills/mxl-validate/SKILL.md create mode 100644 .codex/skills/mxl-validate/scripts/mxl-validate.ps1 create mode 100644 .codex/skills/mxl-validate/scripts/mxl-validate.py create mode 100644 .codex/skills/role-compile/SKILL.md create mode 100644 .codex/skills/role-compile/dsl-reference.md create mode 100644 .codex/skills/role-compile/scripts/role-compile.ps1 create mode 100644 .codex/skills/role-compile/scripts/role-compile.py create mode 100644 .codex/skills/role-info/SKILL.md create mode 100644 .codex/skills/role-info/scripts/role-info.ps1 create mode 100644 .codex/skills/role-info/scripts/role-info.py create mode 100644 .codex/skills/role-validate/SKILL.md create mode 100644 .codex/skills/role-validate/scripts/role-validate.ps1 create mode 100644 .codex/skills/role-validate/scripts/role-validate.py create mode 100644 .codex/skills/skd-compile/SKILL.md create mode 100644 .codex/skills/skd-compile/examples/skd-styles.json create mode 100644 .codex/skills/skd-compile/scripts/skd-compile.ps1 create mode 100644 .codex/skills/skd-compile/scripts/skd-compile.py create mode 100644 .codex/skills/skd-decompile/SKILL.md create mode 100644 .codex/skills/skd-decompile/scripts/skd-decompile.ps1 create mode 100644 .codex/skills/skd-decompile/scripts/skd-decompile.py create mode 100644 .codex/skills/skd-edit/SKILL.md create mode 100644 .codex/skills/skd-edit/scripts/skd-edit.ps1 create mode 100644 .codex/skills/skd-edit/scripts/skd-edit.py create mode 100644 .codex/skills/skd-info/SKILL.md create mode 100644 .codex/skills/skd-info/scripts/skd-info.ps1 create mode 100644 .codex/skills/skd-info/scripts/skd-info.py create mode 100644 .codex/skills/skd-validate/SKILL.md create mode 100644 .codex/skills/skd-validate/scripts/skd-validate.ps1 create mode 100644 .codex/skills/skd-validate/scripts/skd-validate.py create mode 100644 .codex/skills/subsystem-compile/SKILL.md create mode 100644 .codex/skills/subsystem-compile/scripts/subsystem-compile.ps1 create mode 100644 .codex/skills/subsystem-compile/scripts/subsystem-compile.py create mode 100644 .codex/skills/subsystem-edit/SKILL.md create mode 100644 .codex/skills/subsystem-edit/scripts/subsystem-edit.ps1 create mode 100644 .codex/skills/subsystem-edit/scripts/subsystem-edit.py create mode 100644 .codex/skills/subsystem-info/SKILL.md create mode 100644 .codex/skills/subsystem-info/scripts/subsystem-info.ps1 create mode 100644 .codex/skills/subsystem-info/scripts/subsystem-info.py create mode 100644 .codex/skills/subsystem-validate/SKILL.md create mode 100644 .codex/skills/subsystem-validate/scripts/subsystem-validate.ps1 create mode 100644 .codex/skills/subsystem-validate/scripts/subsystem-validate.py create mode 100644 .codex/skills/template-add/SKILL.md create mode 100644 .codex/skills/template-add/scripts/add-template.ps1 create mode 100644 .codex/skills/template-add/scripts/add-template.py create mode 100644 .codex/skills/template-remove/SKILL.md create mode 100644 .codex/skills/template-remove/scripts/remove-template.ps1 create mode 100644 .codex/skills/template-remove/scripts/remove-template.py create mode 100644 .codex/skills/web-info/SKILL.md create mode 100644 .codex/skills/web-info/scripts/web-info.ps1 create mode 100644 .codex/skills/web-info/scripts/web-info.py create mode 100644 .codex/skills/web-publish/SKILL.md create mode 100644 .codex/skills/web-publish/scripts/web-publish.ps1 create mode 100644 .codex/skills/web-publish/scripts/web-publish.py create mode 100644 .codex/skills/web-stop/SKILL.md create mode 100644 .codex/skills/web-stop/scripts/web-stop.ps1 create mode 100644 .codex/skills/web-stop/scripts/web-stop.py create mode 100644 .codex/skills/web-test/.gitignore create mode 100644 .codex/skills/web-test/SKILL.md create mode 100644 .codex/skills/web-test/recording.md create mode 100644 .codex/skills/web-test/regress.md create mode 100644 .codex/skills/web-test/scripts/browser.mjs create mode 100644 .codex/skills/web-test/scripts/cli/commands/exec.mjs create mode 100644 .codex/skills/web-test/scripts/cli/commands/run.mjs create mode 100644 .codex/skills/web-test/scripts/cli/commands/shot.mjs create mode 100644 .codex/skills/web-test/scripts/cli/commands/start.mjs create mode 100644 .codex/skills/web-test/scripts/cli/commands/status.mjs create mode 100644 .codex/skills/web-test/scripts/cli/commands/stop.mjs create mode 100644 .codex/skills/web-test/scripts/cli/commands/test.mjs create mode 100644 .codex/skills/web-test/scripts/cli/exec-context.mjs create mode 100644 .codex/skills/web-test/scripts/cli/server.mjs create mode 100644 .codex/skills/web-test/scripts/cli/session.mjs create mode 100644 .codex/skills/web-test/scripts/cli/test-runner/assertions.mjs create mode 100644 .codex/skills/web-test/scripts/cli/test-runner/discover.mjs create mode 100644 .codex/skills/web-test/scripts/cli/test-runner/reporters.mjs create mode 100644 .codex/skills/web-test/scripts/cli/test-runner/severity.mjs create mode 100644 .codex/skills/web-test/scripts/cli/util.mjs create mode 100644 .codex/skills/web-test/scripts/dom.mjs create mode 100644 .codex/skills/web-test/scripts/dom/_shared.mjs create mode 100644 .codex/skills/web-test/scripts/dom/edd.mjs create mode 100644 .codex/skills/web-test/scripts/dom/edit-state.mjs create mode 100644 .codex/skills/web-test/scripts/dom/errors-stack.mjs create mode 100644 .codex/skills/web-test/scripts/dom/errors.mjs create mode 100644 .codex/skills/web-test/scripts/dom/filter.mjs create mode 100644 .codex/skills/web-test/scripts/dom/form-state.mjs create mode 100644 .codex/skills/web-test/scripts/dom/forms.mjs create mode 100644 .codex/skills/web-test/scripts/dom/grid-edit.mjs create mode 100644 .codex/skills/web-test/scripts/dom/grid.mjs create mode 100644 .codex/skills/web-test/scripts/dom/nav.mjs create mode 100644 .codex/skills/web-test/scripts/dom/submenu.mjs create mode 100644 .codex/skills/web-test/scripts/engine/core/click.mjs create mode 100644 .codex/skills/web-test/scripts/engine/core/clipboard.mjs create mode 100644 .codex/skills/web-test/scripts/engine/core/errors.mjs create mode 100644 .codex/skills/web-test/scripts/engine/core/helpers.mjs create mode 100644 .codex/skills/web-test/scripts/engine/core/scroll-horiz.mjs create mode 100644 .codex/skills/web-test/scripts/engine/core/session.mjs create mode 100644 .codex/skills/web-test/scripts/engine/core/state.mjs create mode 100644 .codex/skills/web-test/scripts/engine/core/wait.mjs create mode 100644 .codex/skills/web-test/scripts/engine/forms/click-form.mjs create mode 100644 .codex/skills/web-test/scripts/engine/forms/click-popup.mjs create mode 100644 .codex/skills/web-test/scripts/engine/forms/close.mjs create mode 100644 .codex/skills/web-test/scripts/engine/forms/fill.mjs create mode 100644 .codex/skills/web-test/scripts/engine/forms/select-value.mjs create mode 100644 .codex/skills/web-test/scripts/engine/forms/state.mjs create mode 100644 .codex/skills/web-test/scripts/engine/nav/navigation.mjs create mode 100644 .codex/skills/web-test/scripts/engine/recording/captions.mjs create mode 100644 .codex/skills/web-test/scripts/engine/recording/capture.mjs create mode 100644 .codex/skills/web-test/scripts/engine/recording/highlight.mjs create mode 100644 .codex/skills/web-test/scripts/engine/recording/narration.mjs create mode 100644 .codex/skills/web-test/scripts/engine/recording/tts.mjs create mode 100644 .codex/skills/web-test/scripts/engine/spreadsheet/spreadsheet.mjs create mode 100644 .codex/skills/web-test/scripts/engine/table/click-cell.mjs create mode 100644 .codex/skills/web-test/scripts/engine/table/click-row.mjs create mode 100644 .codex/skills/web-test/scripts/engine/table/filter.mjs create mode 100644 .codex/skills/web-test/scripts/engine/table/grid-toggle.mjs create mode 100644 .codex/skills/web-test/scripts/engine/table/grid.mjs create mode 100644 .codex/skills/web-test/scripts/engine/table/row-fill.mjs create mode 100644 .codex/skills/web-test/scripts/package-lock.json create mode 100644 .codex/skills/web-test/scripts/package.json create mode 100644 .codex/skills/web-test/scripts/run.mjs create mode 100644 .codex/skills/web-unpublish/SKILL.md create mode 100644 .codex/skills/web-unpublish/scripts/web-unpublish.ps1 create mode 100644 .codex/skills/web-unpublish/scripts/web-unpublish.py create mode 100644 LICENSE create mode 100644 README.md diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json new file mode 100644 index 00000000..644720d6 --- /dev/null +++ b/.codex-plugin/plugin.json @@ -0,0 +1,36 @@ +{ + "name": "1c-skills", + "version": "2026.6.4+6d119eb", + "description": "[PowerShell] Навыки для разработки на 1С:Предприятие 8.3 — абстракции над XML-форматами и CLI конфигуратора, плюс глаза и руки для тестирования через веб-клиент.", + "author": { + "name": "Nikolay Shirokov" + }, + "homepage": "https://github.com/Nikolay-Shirokov/cc-1c-skills", + "repository": "https://github.com/Nikolay-Shirokov/cc-1c-skills", + "license": "MIT", + "keywords": [ + "1c", + "1c-dev", + "cf", + "cfe", + "epf", + "erf", + "metadata", + "configuration", + "extension", + "form", + "report", + "skd", + "data-processor", + "mxl", + "web-client", + "testing", + "test-automation" + ], + "skills": "./.codex/skills/", + "interface": { + "displayName": "1C Skills (PowerShell)", + "shortDescription": "PowerShell runtime (Windows-first)", + "category": "Development" + } +} diff --git a/.codex/skills/.gitignore b/.codex/skills/.gitignore new file mode 100644 index 00000000..58200d4d --- /dev/null +++ b/.codex/skills/.gitignore @@ -0,0 +1 @@ +__pycache__/ diff --git a/.codex/skills/cf-edit/SKILL.md b/.codex/skills/cf-edit/SKILL.md new file mode 100644 index 00000000..81155ebd --- /dev/null +++ b/.codex/skills/cf-edit/SKILL.md @@ -0,0 +1,60 @@ +--- +name: cf-edit +description: Точечное редактирование конфигурации 1С. Используй когда нужно изменить свойства конфигурации, добавить или удалить объект из состава, настроить роли по умолчанию, поменять раскладку панелей, настроить начальную страницу +argument-hint: -ConfigPath -Operation -Value +allowed-tools: + - Bash + - Read + - Write + - Glob +--- + +# /cf-edit — редактирование конфигурации 1С + +Точечное редактирование Configuration.xml: свойства, состав ChildObjects, роли по умолчанию. + +## Параметры и команда + +| Параметр | Описание | +|----------|----------| +| `ConfigPath` | Путь к Configuration.xml или каталогу выгрузки | +| `Operation` | Операция (см. таблицу) | +| `Value` | Значение для операции (batch через `;;`) | +| `DefinitionFile` | JSON-файл с массивом операций | +| `NoValidate` | Пропустить авто-валидацию | + +```powershell +powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/cf-edit/scripts/cf-edit.ps1" -ConfigPath '' -Operation modify-property -Value 'Version=1.0.0.1' +``` + +## Операции + +| Операция | Формат Value | Описание | +|----------|-------------|----------| +| `modify-property` | `Ключ=Значение` (batch `;;`) | Изменить свойство | +| `add-childObject` | `Type.Name` (batch `;;`) | Зарегистрировать уже существующий файл объекта в ChildObjects. Для создания нового объекта используй `/meta-compile`, `/role-compile`, `/subsystem-compile` — они регистрируют автоматически | +| `remove-childObject` | `Type.Name` (batch `;;`) | Удалить объект из ChildObjects | +| `add-defaultRole` | `Role.Name` или `Name` | Добавить роль по умолчанию | +| `remove-defaultRole` | `Role.Name` или `Name` | Удалить роль по умолчанию | +| `set-defaultRoles` | Имена через `;;` | Заменить список ролей по умолчанию | +| `set-panels` | JSON-объект (см. [reference.md](reference.md)) | Перезаписать `Ext/ClientApplicationInterface.xml` (раскладка панелей) | +| `set-home-page` | JSON-объект (см. [reference.md](reference.md)) | Перезаписать `Ext/HomePageWorkArea.xml` (начальная страница) | + +Допустимые значения свойств, формат DefinitionFile (JSON), каноничный порядок: [reference.md](reference.md) + +## Примеры + +```powershell +# Изменить версию и поставщика +... -ConfigPath src -Operation modify-property -Value "Version=1.0.0.1 ;; Vendor=Фирма 1С" + +# Добавить объекты +... -ConfigPath src -Operation add-childObject -Value "Catalog.Товары ;; Document.Заказ" + +# Удалить объект +... -ConfigPath src -Operation remove-childObject -Value "Catalog.Устаревший" + +# Роли по умолчанию +... -ConfigPath src -Operation add-defaultRole -Value "ПолныеПрава" +... -ConfigPath src -Operation set-defaultRoles -Value "ПолныеПрава ;; Администратор" +``` diff --git a/.codex/skills/cf-edit/reference.md b/.codex/skills/cf-edit/reference.md new file mode 100644 index 00000000..efaa5231 --- /dev/null +++ b/.codex/skills/cf-edit/reference.md @@ -0,0 +1,150 @@ +# cf-edit — справочник операций + +## modify-property + +Свойства для редактирования: + +### Скалярные +`Name`, `Version`, `Vendor`, `Comment`, `NamePrefix`, `UpdateCatalogAddress` + +### LocalString (многоязычные) +`Synonym`, `BriefInformation`, `DetailedInformation`, `Copyright`, `VendorInformationAddress`, `ConfigurationInformationAddress` + +### Enum +| Свойство | Допустимые значения | +|----------|---------------------| +| `CompatibilityMode` | `Version8_3_20` ... `Version8_3_28`, `Version8_5_1`, `DontUse` | +| `ConfigurationExtensionCompatibilityMode` | то же | +| `DefaultRunMode` | `ManagedApplication`, `OrdinaryApplication`, `Auto` | +| `ScriptVariant` | `Russian`, `English` | +| `DataLockControlMode` | `Managed`, `Automatic`, `AutomaticAndManaged` | +| `ObjectAutonumerationMode` | `NotAutoFree`, `AutoFree` | +| `ModalityUseMode` | `DontUse`, `Use`, `UseWithWarnings` | +| `SynchronousPlatformExtensionAndAddInCallUseMode` | `DontUse`, `Use`, `UseWithWarnings` | +| `InterfaceCompatibilityMode` | `Version8_2`, `Version8_2EnableTaxi`, `Taxi`, `TaxiEnableVersion8_2`, `TaxiEnableVersion8_5`, `Version8_5EnableTaxi`, `Version8_5` | +| `DatabaseTablespacesUseMode` | `DontUse`, `Use` | +| `MainClientApplicationWindowMode` | `Normal`, `Fullscreen`, `Kiosk` | + +### Ref +`DefaultLanguage` — значение вида `Language.Русский` + +### Формат batch +`"Version=1.0.0.1 ;; Vendor=Фирма 1С ;; Synonym=Тестовая конфигурация"` + +## add-childObject / remove-childObject + +Формат: `Type.Name` — XML-тип и имя объекта через точку. + +**Важно про `add-childObject`**: регистрирует в `` объект, **файл которого уже существует на диске**. Если файла нет — exit 1. Для создания нового объекта используй профильный навык — `/meta-compile` (Catalog, Document, Enum, Report, регистры и т.д.), `/role-compile` (Role), `/subsystem-compile` (Subsystem). Они создают файл И регистрируют его за один вызов. + +Batch: `"Catalog.Товары ;; Document.Заказ ;; Enum.ВидыОплат"` + +## add-defaultRole / remove-defaultRole / set-defaultRoles + +Имя роли: `ПолныеПрава` или `Role.ПолныеПрава` (префикс `Role.` добавляется автоматически). + +`set-defaultRoles` полностью заменяет список ролей. + +## set-panels + +Перезаписывает `Ext/ClientApplicationInterface.xml` — раскладку панелей рабочего пространства Taxi. Файл создаётся с нуля; то, что не упомянуто в `value`, отсутствует на экране. + +`value` — объект с ключами `top`, `left`, `right`, `bottom`. Каждый ключ — массив записей. Ключ можно опустить (= пустая сторона). + +**Запись** — одна из: +- Строка-алиас (одна панель в этом слоте) +- Объект `{"group": [...]}` (стек: панели/подгруппы внутри располагаются друг под другом) + +**Алиасы панелей:** + +| Алиас | Панель | +|-------|--------| +| `sections` | Панель разделов | +| `open` | Панель открытых | +| `favorites` | Панель избранного | +| `history` | Панель истории | +| `functions` | Панель функций текущего раздела | + +**Семантика:** +- Несколько записей в одной стороне → отдельные слоты «рядом» (несколько тегов ``/...) +- `{"group":[...]}` → один тег с ``-обёрткой, элементы внутри идут стеком + +**Пример** (DefinitionFile): +```json +[ + { + "operation": "set-panels", + "value": { + "top": ["open"], + "left": ["sections"], + "right": [{ "group": ["favorites", "history"] }], + "bottom": ["functions"] + } + } +] +``` + +Через `-Value` (CLI): передай объект как JSON-строку — `... -Operation set-panels -Value '{"top":["open"]}'`. + +## set-home-page + +Перезаписывает `Ext/HomePageWorkArea.xml` — раскладка форм на начальной странице (рабочая область). Файл создаётся с нуля; то, что не упомянуто в `value`, отсутствует. + +`value` — объект: + +| Ключ | Канонич. (XML) | Описание | +|------|----------------|----------| +| `template` | `WorkingAreaTemplate` | `OneColumn` / `TwoColumnsEqualWidth` (дефолт) / `TwoColumnsVariableWidth` | +| `left` | `LeftColumn` | массив записей форм | +| `right` | `RightColumn` | массив записей форм (запрещён при `OneColumn`) | + +Принимаются и короткие и канонич. ключи (XML-имена) — оба работают. + +**Запись формы** — одна из: +- Строка `"
"` — только имя формы, дефолты `height=10`, `visibility=true` +- Объект `{form, height?, visibility?, roles?}` + +| Поле | Канонич. | Дефолт | Описание | +|------|----------|--------|----------| +| `form` | `Form` | — | `CommonForm.X` или `Type.Object.Form.Name` (или UUID) | +| `height` | `Height` | `10` | Высота | +| `visibility` | `Visibility` | `true` | Общая видимость (``) | +| `roles` | — | — | `{"Role.Имя": true|false, ...}` — переопределения по ролям | + +**Семантика visibility:** `visibility` = общее правило, `roles` — точечные исключения. Скрыть для всех кроме одной роли: `{"visibility": false, "roles": {"Role.Опер": true}}`. + +**Пример:** +```json +[ + { + "operation": "set-home-page", + "value": { + "template": "TwoColumnsVariableWidth", + "left": [ + "CommonForm.НачалоРаботы", + { "form": "CommonForm.СписокЗадач", "height": 100, "visibility": false }, + { "form": "Catalog.Контрагенты.Form.ФормаСписка", "height": 50 }, + { + "form": "CommonForm.РабочийСтолОператора", + "visibility": false, + "roles": { "Role.Оператор": true, "Role.ПолныеПрава": false } + } + ], + "right": [ + { "form": "DataProcessor.Поиск.Form.ФормаПоиска", "height": 30 } + ] + } + } +] +``` + +## DefinitionFile (JSON) + +```json +[ + { "operation": "modify-property", "value": "Version=2.0.0.1 ;; Vendor=Test" }, + { "operation": "add-childObject", "value": "Catalog.Товары ;; Document.Заказ" }, + { "operation": "add-defaultRole", "value": "ПолныеПрава" } +] +``` + diff --git a/.codex/skills/cf-edit/scripts/cf-edit.ps1 b/.codex/skills/cf-edit/scripts/cf-edit.ps1 new file mode 100644 index 00000000..5da5fc4f --- /dev/null +++ b/.codex/skills/cf-edit/scripts/cf-edit.ps1 @@ -0,0 +1,869 @@ +# cf-edit v1.4 — Edit 1C configuration root (Configuration.xml) +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( + [Parameter(Mandatory)][Alias('Path')][string]$ConfigPath, + [string]$DefinitionFile, + [ValidateSet("modify-property","add-childObject","remove-childObject","add-defaultRole","remove-defaultRole","set-defaultRoles","set-panels","set-home-page")] + [string]$Operation, + [string]$Value, + [switch]$NoValidate +) + +$ErrorActionPreference = "Stop" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- Mode validation --- +if ($DefinitionFile -and $Operation) { Write-Error "Cannot use both -DefinitionFile and -Operation"; exit 1 } +if (-not $DefinitionFile -and -not $Operation) { Write-Error "Either -DefinitionFile or -Operation is required"; exit 1 } + +# --- Resolve path --- +if (-not [System.IO.Path]::IsPathRooted($ConfigPath)) { + $ConfigPath = Join-Path (Get-Location).Path $ConfigPath +} +if (Test-Path $ConfigPath -PathType Container) { + $candidate = Join-Path $ConfigPath "Configuration.xml" + if (Test-Path $candidate) { $ConfigPath = $candidate } + else { Write-Error "No Configuration.xml in directory"; exit 1 } +} +if (-not (Test-Path $ConfigPath)) { Write-Error "File not found: $ConfigPath"; exit 1 } +$resolvedPath = (Resolve-Path $ConfigPath).Path +$script:configDir = [System.IO.Path]::GetDirectoryName($resolvedPath) + +# --- Load XML with PreserveWhitespace --- +$script:xmlDoc = New-Object System.Xml.XmlDocument +$script:xmlDoc.PreserveWhitespace = $true +$script:xmlDoc.Load($resolvedPath) + +$script:addCount = 0 +$script:removeCount = 0 +$script:modifyCount = 0 + +function Info([string]$msg) { Write-Host "[INFO] $msg" } +function Warn([string]$msg) { Write-Host "[WARN] $msg" } + +# --- Detect structure --- +$root = $script:xmlDoc.DocumentElement +$script:mdNs = "http://v8.1c.ru/8.3/MDClasses" +$script:xrNs = "http://v8.1c.ru/8.3/xcf/readable" +$script:xsiNs = "http://www.w3.org/2001/XMLSchema-instance" +$script:v8Ns = "http://v8.1c.ru/8.1/data/core" + +$script:cfgEl = $null +foreach ($child in $root.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Configuration") { + $script:cfgEl = $child; break + } +} +if (-not $script:cfgEl) { Write-Error "No element found"; exit 1 } + +$script:propsEl = $null +$script:childObjsEl = $null +foreach ($child in $script:cfgEl.ChildNodes) { + if ($child.NodeType -ne 'Element') { continue } + if ($child.LocalName -eq "Properties") { $script:propsEl = $child } + if ($child.LocalName -eq "ChildObjects") { $script:childObjsEl = $child } +} + +$script:objName = "" +foreach ($child in $script:propsEl.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Name") { + $script:objName = $child.InnerText.Trim(); break + } +} +Info "Configuration: $($script:objName)" + +# --- Canonical type order for ChildObjects (44 types) --- +$script:typeOrder = @( + "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) --- +$script:typeToDir = @{ + "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" +} + +# --- XML manipulation helpers (from subsystem-edit pattern) --- +function Get-ChildIndent($container) { + foreach ($child in $container.ChildNodes) { + if ($child.NodeType -eq 'Whitespace' -or $child.NodeType -eq 'SignificantWhitespace') { + if ($child.Value -match '^\r?\n(\t+)$') { return $Matches[1] } + if ($child.Value -match '^\r?\n(\t+)') { return $Matches[1] } + } + } + $depth = 0; $current = $container + while ($current -and $current -ne $script:xmlDoc.DocumentElement) { $depth++; $current = $current.ParentNode } + return "`t" * ($depth + 1) +} + +function Insert-BeforeElement($container, $newNode, $refNode, $childIndent) { + $ws = $script:xmlDoc.CreateWhitespace("`r`n$childIndent") + if ($refNode) { + $container.InsertBefore($ws, $refNode) | Out-Null + $container.InsertBefore($newNode, $ws) | Out-Null + } else { + $trailing = $container.LastChild + if ($trailing -and ($trailing.NodeType -eq 'Whitespace' -or $trailing.NodeType -eq 'SignificantWhitespace')) { + $container.InsertBefore($ws, $trailing) | Out-Null + $container.InsertBefore($newNode, $trailing) | Out-Null + } else { + $container.AppendChild($ws) | Out-Null + $container.AppendChild($newNode) | Out-Null + $parentIndent = if ($childIndent.Length -gt 1) { $childIndent.Substring(0, $childIndent.Length - 1) } else { "" } + $closeWs = $script:xmlDoc.CreateWhitespace("`r`n$parentIndent") + $container.AppendChild($closeWs) | Out-Null + } + } +} + +function Remove-NodeWithWhitespace($node) { + $parent = $node.ParentNode + $prev = $node.PreviousSibling + $next = $node.NextSibling + if ($prev -and ($prev.NodeType -eq 'Whitespace' -or $prev.NodeType -eq 'SignificantWhitespace')) { + $parent.RemoveChild($prev) | Out-Null + } elseif ($next -and ($next.NodeType -eq 'Whitespace' -or $next.NodeType -eq 'SignificantWhitespace')) { + $parent.RemoveChild($next) | Out-Null + } + $parent.RemoveChild($node) | Out-Null +} + +function Expand-SelfClosingElement($container, $parentIndent) { + if (-not $container.HasChildNodes -or $container.IsEmpty) { + $closeWs = $script:xmlDoc.CreateWhitespace("`r`n$parentIndent") + $container.AppendChild($closeWs) | Out-Null + } +} + +function Import-Fragment([string]$xmlString) { + $wrapper = "<_W xmlns=`"$($script:mdNs)`" xmlns:xsi=`"$($script:xsiNs)`" xmlns:v8=`"$($script:v8Ns)`" xmlns:xr=`"$($script:xrNs)`" xmlns:xs=`"http://www.w3.org/2001/XMLSchema`">$xmlString" + $frag = New-Object System.Xml.XmlDocument + $frag.PreserveWhitespace = $true + $frag.LoadXml($wrapper) + $nodes = @() + foreach ($child in $frag.DocumentElement.ChildNodes) { + if ($child.NodeType -eq 'Element') { + $nodes += $script:xmlDoc.ImportNode($child, $true) + } + } + return ,$nodes +} + +# --- Parse batch value (split by ;;) --- +function Parse-BatchValue([string]$val) { + $items = @() + foreach ($part in $val.Split(";;")) { + $trimmed = $part.Trim() + if ($trimmed) { $items += $trimmed } + } + return ,$items +} + +# --- LocalString properties --- +$mlProps = @("Synonym","BriefInformation","DetailedInformation","Copyright","VendorInformationAddress","ConfigurationInformationAddress") +# Scalar properties +$scalarProps = @("Name","Version","Vendor","Comment","NamePrefix","UpdateCatalogAddress") +# Ref properties +$refProps = @("DefaultLanguage") + +# --- Operation: modify-property --- +function Do-ModifyProperty([string]$batchVal) { + $items = Parse-BatchValue $batchVal + foreach ($item in $items) { + $eqIdx = $item.IndexOf("=") + if ($eqIdx -lt 1) { + Write-Error "Invalid property format '$item', expected 'Key=Value'" + exit 1 + } + $propName = $item.Substring(0, $eqIdx).Trim() + $propValue = $item.Substring($eqIdx + 1).Trim() + + # Find property element + $propEl = $null + foreach ($child in $script:propsEl.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq $propName) { + $propEl = $child; break + } + } + if (-not $propEl) { + Write-Error "Property '$propName' not found in Properties" + exit 1 + } + + if ($mlProps -contains $propName) { + # LocalString + if (-not $propValue) { + $propEl.InnerXml = "" + } else { + $indent = Get-ChildIndent $script:propsEl + $escaped = [System.Security.SecurityElement]::Escape($propValue) + $mlXml = "`r`n$indent`t`r`n$indent`t`tru`r`n$indent`t`t$escaped`r`n$indent`t`r`n$indent" + $propEl.InnerXml = $mlXml + } + } elseif ($scalarProps -contains $propName -or $refProps -contains $propName) { + # Simple text + if (-not $propValue) { $propEl.InnerXml = "" } + else { $propEl.InnerText = $propValue } + } else { + # Enum or other — just set text + $propEl.InnerText = $propValue + } + + $script:modifyCount++ + Info "Set $propName = `"$propValue`"" + } +} + +# --- Operation: add-childObject --- +function Do-AddChildObject([string]$batchVal) { + if (-not $script:childObjsEl) { Write-Error "No element found"; exit 1 } + + $items = Parse-BatchValue $batchVal + $cfgIndent = Get-ChildIndent $script:cfgEl + + # Expand self-closing if needed + if (-not $script:childObjsEl.HasChildNodes -or $script:childObjsEl.IsEmpty) { + Expand-SelfClosingElement $script:childObjsEl $cfgIndent + } + $childIndent = Get-ChildIndent $script:childObjsEl + + foreach ($item in $items) { + $dotIdx = $item.IndexOf(".") + if ($dotIdx -lt 1) { + Write-Error "Invalid format '$item', expected 'Type.Name'" + exit 1 + } + $typeName = $item.Substring(0, $dotIdx) + $objNameVal = $item.Substring($dotIdx + 1) + + # Check type is valid + $typeIdx = $script:typeOrder.IndexOf($typeName) + if ($typeIdx -lt 0) { + Write-Error "Unknown type '$typeName'" + exit 1 + } + + # 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. + $typeDir = $script:typeToDir[$typeName] + $objFile = Join-Path (Join-Path $script:configDir $typeDir) "$objNameVal.xml" + if (-not (Test-Path $objFile)) { + $hintSkill = switch ($typeName) { + "Subsystem" { "subsystem-compile" } + "Role" { "role-compile" } + default { "meta-compile" } + } + Write-Error @" +Object file not found: $typeDir/$objNameVal.xml +cf-edit add-childObject only references objects that already exist on disk. +To create a new $typeName, use $hintSkill (auto-registers in Configuration.xml): + /$hintSkill with {"type":"$typeName","name":"$objNameVal"} +"@ + exit 1 + } + + # Dedup check + $existing = $false + foreach ($child in $script:childObjsEl.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq $typeName -and $child.InnerText -eq $objNameVal) { + $existing = $true; break + } + } + if ($existing) { + Warn "Already exists: $typeName.$objNameVal" + continue + } + + # Find insertion point: after last element of same type, or after last element of preceding type + $insertBefore = $null + $lastSameType = $null + $lastPrecedingType = $null + $currentTypeIdx = -1 + + foreach ($child in $script:childObjsEl.ChildNodes) { + if ($child.NodeType -ne 'Element') { continue } + $childTypeIdx = $script:typeOrder.IndexOf($child.LocalName) + if ($childTypeIdx -lt 0) { continue } + + if ($child.LocalName -eq $typeName) { + # Same type — check alphabetical order + if ($child.InnerText -gt $objNameVal -and -not $insertBefore) { + # Insert before this element (alphabetical) + $insertBefore = $child + } + $lastSameType = $child + } elseif ($childTypeIdx -lt $typeIdx) { + $lastPrecedingType = $child + } elseif ($childTypeIdx -gt $typeIdx -and -not $insertBefore) { + # First element of a later type — insert before it + $insertBefore = $child + } + } + + # Create element + $newEl = $script:xmlDoc.CreateElement($typeName, $script:mdNs) + $newEl.InnerText = $objNameVal + + if ($insertBefore) { + Insert-BeforeElement $script:childObjsEl $newEl $insertBefore $childIndent + } else { + # Append at end (or after last same/preceding type) + Insert-BeforeElement $script:childObjsEl $newEl $null $childIndent + } + + $script:addCount++ + Info "Added: $typeName.$objNameVal" + } +} + +# --- Operation: remove-childObject --- +function Do-RemoveChildObject([string]$batchVal) { + if (-not $script:childObjsEl) { Write-Error "No element found"; exit 1 } + + $items = Parse-BatchValue $batchVal + foreach ($item in $items) { + $dotIdx = $item.IndexOf(".") + if ($dotIdx -lt 1) { + Write-Error "Invalid format '$item', expected 'Type.Name'" + exit 1 + } + $typeName = $item.Substring(0, $dotIdx) + $objNameVal = $item.Substring($dotIdx + 1) + + $found = $false + foreach ($child in @($script:childObjsEl.ChildNodes)) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq $typeName -and $child.InnerText -eq $objNameVal) { + Remove-NodeWithWhitespace $child + $script:removeCount++ + Info "Removed: $typeName.$objNameVal" + $found = $true + break + } + } + if (-not $found) { Warn "Not found: $typeName.$objNameVal" } + } +} + +# --- Operation: add-defaultRole --- +function Do-AddDefaultRole([string]$batchVal) { + $items = Parse-BatchValue $batchVal + + # Find DefaultRoles element + $rolesEl = $null + foreach ($child in $script:propsEl.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "DefaultRoles") { + $rolesEl = $child; break + } + } + if (-not $rolesEl) { Write-Error "No element found in Properties"; exit 1 } + + $propsIndent = Get-ChildIndent $script:propsEl + if (-not $rolesEl.HasChildNodes -or $rolesEl.IsEmpty) { + Expand-SelfClosingElement $rolesEl $propsIndent + } + $roleIndent = Get-ChildIndent $rolesEl + + foreach ($item in $items) { + $roleName = $item + if (-not $roleName.StartsWith("Role.")) { $roleName = "Role.$roleName" } + + # Dedup + $existing = $false + foreach ($child in $rolesEl.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.InnerText.Trim() -eq $roleName) { + $existing = $true; break + } + } + if ($existing) { + Warn "DefaultRole already exists: $roleName" + continue + } + + $fragXml = "$roleName" + $nodes = Import-Fragment $fragXml + if ($nodes.Count -gt 0) { + Insert-BeforeElement $rolesEl $nodes[0] $null $roleIndent + $script:addCount++ + Info "Added DefaultRole: $roleName" + } + } +} + +# --- Operation: remove-defaultRole --- +function Do-RemoveDefaultRole([string]$batchVal) { + $items = Parse-BatchValue $batchVal + + $rolesEl = $null + foreach ($child in $script:propsEl.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "DefaultRoles") { + $rolesEl = $child; break + } + } + if (-not $rolesEl) { Write-Error "No element found"; exit 1 } + + foreach ($item in $items) { + $roleName = $item + if (-not $roleName.StartsWith("Role.")) { $roleName = "Role.$roleName" } + + $found = $false + foreach ($child in @($rolesEl.ChildNodes)) { + if ($child.NodeType -eq 'Element' -and $child.InnerText.Trim() -eq $roleName) { + Remove-NodeWithWhitespace $child + $script:removeCount++ + Info "Removed DefaultRole: $roleName" + $found = $true + break + } + } + if (-not $found) { Warn "DefaultRole not found: $roleName" } + } +} + +# --- Operation: set-panels --- +# Canonical English aliases — preferred form, used in docs and error messages. +$script:panelUuids = @{ + "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). +$script:panelSynonyms = @{ + "разделов" = "sections"; "разделы" = "sections" + "открытых" = "open"; "открытые" = "open" + "избранного" = "favorites";"избранное" = "favorites" + "истории" = "history"; "история" = "history" + "функций" = "functions";"функции" = "functions" +} + +function Build-PanelEntryXml($entry, [string]$indent) { + # String alias -> ... + if ($entry -is [string]) { + $key = $entry.ToLowerInvariant() + if ($script:panelSynonyms.ContainsKey($key)) { $key = $script:panelSynonyms[$key] } + if (-not $script:panelUuids.ContainsKey($key)) { + Write-Error "Unknown panel alias '$entry'. Allowed: $(($script:panelUuids.Keys | Sort-Object) -join ', ')" + exit 1 + } + $u = $script:panelUuids[$key] + $instId = [guid]::NewGuid().ToString() + return "$indent`r`n$indent`t$u`r`n$indent" + } + # Object {group: [...]} -> ... (stack) + if ($entry.PSObject.Properties['group']) { + $children = $entry.group + if (-not $children -or $children.Count -eq 0) { + Write-Error "group must contain at least one entry" + exit 1 + } + $gid = [guid]::NewGuid().ToString() + $inner = "" + foreach ($child in $children) { + $childXml = Build-PanelEntryXml $child "$indent`t`t" + $inner += "$indent`t`r`n$childXml`r`n$indent`t`r`n" + } + return "$indent`r`n$inner$indent" + } + Write-Error "Panel entry must be a string alias or object {group:[...]}, got: $($entry | ConvertTo-Json -Compress)" + exit 1 +} + +function Do-SetPanels($valArg) { + # Accept string (JSON), PSCustomObject, or hashtable + $layout = $valArg + if ($layout -is [string]) { + try { $layout = $layout | ConvertFrom-Json } catch { + Write-Error "set-panels value must be valid JSON object, got: $valArg" + exit 1 + } + } + if (-not $layout) { + Write-Error "set-panels value is empty" + exit 1 + } + + $sides = @("top","left","right","bottom") + $bodyParts = @() + foreach ($side in $sides) { + $entries = $null + if ($layout.PSObject.Properties[$side]) { $entries = $layout.$side } + if ($null -eq $entries) { continue } + # Normalize to array + if ($entries -isnot [System.Array] -and $entries -isnot [System.Collections.IList]) { + $entries = @($entries) + } + foreach ($entry in $entries) { + $entryXml = Build-PanelEntryXml $entry "`t`t" + $bodyParts += "`t<$side>`r`n$entryXml`r`n`t" + } + } + + # Reject unknown side keys (catches typos like "Top" vs "top") + foreach ($prop in $layout.PSObject.Properties) { + if ($sides -notcontains $prop.Name) { + Write-Error "Unknown side '$($prop.Name)'. Allowed: $($sides -join ', ')" + exit 1 + } + } + + $body = $bodyParts -join "`r`n" + $declarations = @" + + + + + +"@ + $bodyBlock = if ($body) { "$body`r`n" } else { "" } + $caiXml = @" + + +$bodyBlock$declarations + +"@ + + $extDir = Join-Path $script:configDir "Ext" + if (-not (Test-Path $extDir)) { New-Item -ItemType Directory -Path $extDir -Force | Out-Null } + $caiPath = Join-Path $extDir "ClientApplicationInterface.xml" + $utf8Bom = New-Object System.Text.UTF8Encoding($true) + [System.IO.File]::WriteAllText($caiPath, $caiXml, $utf8Bom) + $script:modifyCount++ + Info "Wrote panel layout: $caiPath" +} + +# --- Operation: set-home-page --- +# Russian → English type aliases for form-ref normalization +$script:ruTypeMap = @{ + "справочник" = "Catalog" + "документ" = "Document" + "перечисление" = "Enum" + "отчёт" = "Report" + "отчет" = "Report" + "обработка" = "DataProcessor" + "общаяформа" = "CommonForm" + "журналдокументов" = "DocumentJournal" + "планвидовхарактеристик" = "ChartOfCharacteristicTypes" + "плансчетов" = "ChartOfAccounts" + "планвидоврасчета" = "ChartOfCalculationTypes" + "планвидоврасчёта" = "ChartOfCalculationTypes" + "регистрсведений" = "InformationRegister" + "регистрнакопления" = "AccumulationRegister" + "регистрбухгалтерии" = "AccountingRegister" + "регистррасчета" = "CalculationRegister" + "регистррасчёта" = "CalculationRegister" + "бизнеспроцесс" = "BusinessProcess" + "задача" = "Task" + "планобмена" = "ExchangePlan" + "хранилищенастроек" = "SettingsStorage" +} +# plural folder → singular type +$script:dirToType = @{} +foreach ($k in $script:typeToDir.Keys) { $script:dirToType[$script:typeToDir[$k].ToLowerInvariant()] = $k } + +function Normalize-FormRef([string]$s) { + $s = $s.Trim() + if (-not $s) { return $s } + # UUID — leave as-is + if ($s -match '^[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}$') { return $s } + # Path form? + if ($s.Contains("/") -or $s.Contains("\")) { + $parts = $s.Replace("\","/").Split("/") | Where-Object { $_ -ne "" -and $_.ToLowerInvariant() -ne "ext" } + # Strip trailing Form.xml + if ($parts.Count -gt 0 -and $parts[-1].ToLowerInvariant() -eq "form.xml") { + $parts = @($parts[0..($parts.Count - 2)]) + } + if ($parts.Count -ge 2) { + $typeDir = $parts[0] + $typeSingular = $script:dirToType[$typeDir.ToLowerInvariant()] + if ($typeSingular) { + if ($typeSingular -eq "CommonForm" -and $parts.Count -ge 2) { + return "CommonForm.$($parts[1])" + } + if ($parts.Count -ge 4 -and $parts[2].ToLowerInvariant() -eq "forms") { + return "$typeSingular.$($parts[1]).Form.$($parts[3])" + } + } + } + return $s + } + # Dot form — translate Russian head and 'Форма' segment, auto-insert 'Form' + $segs = $s.Split(".") + if ($segs.Count -ge 1) { + $head = $segs[0].ToLowerInvariant() + if ($script:ruTypeMap.ContainsKey($head)) { $segs[0] = $script:ruTypeMap[$head] } + for ($i = 1; $i -lt $segs.Count; $i++) { + if ($segs[$i] -eq "Форма") { $segs[$i] = "Form" } + } + # Auto-insert Form: for object types with 3 segments (Type.Object.FormName) + if ($segs.Count -eq 3 -and $script:typeOrder -contains $segs[0] -and $segs[0] -ne "CommonForm") { + $segs = @($segs[0], $segs[1], "Form", $segs[2]) + } + } + return ($segs -join ".") +} + +# Accept short DSL or canonical XML keys (silently) +function Get-FieldValue($obj, [string[]]$keys) { + foreach ($k in $keys) { + if ($obj.PSObject.Properties[$k]) { return $obj.PSObject.Properties[$k].Value } + } + return $null +} + +function Build-HomePageItemXml($entry, [string]$indent) { + # Resolve fields + if ($entry -is [string]) { + $formRef = Normalize-FormRef $entry + $height = 10 + $common = $true + $roles = $null + } else { + $formRaw = Get-FieldValue $entry @("form","Form") + if (-not $formRaw) { Write-Error "Home page item: 'form' is required, got: $($entry | ConvertTo-Json -Compress)"; exit 1 } + $formRef = Normalize-FormRef ([string]$formRaw) + $h = Get-FieldValue $entry @("height","Height") + $height = if ($null -ne $h) { [int]$h } else { 10 } + $vis = Get-FieldValue $entry @("visibility","Visibility") + $common = if ($null -ne $vis) { [bool]$vis } else { $true } + $roles = Get-FieldValue $entry @("roles") + } + + $visParts = @() + $visParts += "$indent`t`t$($common.ToString().ToLower())" + if ($roles) { + # roles is PSCustomObject {Role.X: bool, ...} + foreach ($prop in $roles.PSObject.Properties) { + $rname = $prop.Name + if (-not $rname.StartsWith("Role.") -and -not ($rname -match '^[0-9a-fA-F]{8}-')) { $rname = "Role.$rname" } + $rval = ([bool]$prop.Value).ToString().ToLower() + $escName = [System.Security.SecurityElement]::Escape($rname) + $visParts += "$indent`t`t$rval" + } + } + $visBlock = $visParts -join "`r`n" + $escForm = [System.Security.SecurityElement]::Escape($formRef) + return @" +$indent +$indent`t$escForm +$indent`t$height +$indent`t +$visBlock +$indent`t +$indent +"@ +} + +function Do-SetHomePage($valArg) { + $layout = $valArg + if ($layout -is [string]) { + try { $layout = $layout | ConvertFrom-Json } catch { + Write-Error "set-home-page value must be valid JSON object"; exit 1 + } + } + if (-not $layout) { Write-Error "set-home-page value is empty"; exit 1 } + + $allowedTemplates = @("OneColumn","TwoColumnsEqualWidth","TwoColumnsVariableWidth") + $tmpl = Get-FieldValue $layout @("template","WorkingAreaTemplate") + if (-not $tmpl) { $tmpl = "TwoColumnsEqualWidth" } + if ($allowedTemplates -notcontains $tmpl) { + Write-Error "Unknown template '$tmpl'. Allowed: $($allowedTemplates -join ', ')"; exit 1 + } + + $leftItems = Get-FieldValue $layout @("left","LeftColumn") + $rightItems = Get-FieldValue $layout @("right","RightColumn") + + # Reject unknown keys + $known = @("template","WorkingAreaTemplate","left","LeftColumn","right","RightColumn") + foreach ($prop in $layout.PSObject.Properties) { + if ($known -notcontains $prop.Name) { + Write-Error "Unknown key '$($prop.Name)'. Allowed: template, left, right"; exit 1 + } + } + + if ($tmpl -eq "OneColumn" -and $rightItems) { + Write-Error "Template 'OneColumn' cannot have items in 'right' column"; exit 1 + } + + function Build-Column([string]$tag, $items) { + if (-not $items) { return "`t<$tag/>" } + if ($items -isnot [System.Array] -and $items -isnot [System.Collections.IList]) { + $items = @($items) + } + if ($items.Count -eq 0) { return "`t<$tag/>" } + $itemBlocks = @() + foreach ($it in $items) { + $itemBlocks += Build-HomePageItemXml $it "`t`t" + } + $body = $itemBlocks -join "`r`n" + return "`t<$tag>`r`n$body`r`n`t" + } + + $leftXml = Build-Column "LeftColumn" $leftItems + $rightXml = Build-Column "RightColumn" $rightItems + + $hpXml = @" + + + $tmpl +$leftXml +$rightXml + +"@ + + $extDir = Join-Path $script:configDir "Ext" + if (-not (Test-Path $extDir)) { New-Item -ItemType Directory -Path $extDir -Force | Out-Null } + $hpPath = Join-Path $extDir "HomePageWorkArea.xml" + $utf8Bom = New-Object System.Text.UTF8Encoding($true) + [System.IO.File]::WriteAllText($hpPath, $hpXml, $utf8Bom) + $script:modifyCount++ + Info "Wrote home page layout: $hpPath" +} + +# --- Operation: set-defaultRoles --- +function Do-SetDefaultRoles([string]$batchVal) { + $items = Parse-BatchValue $batchVal + + $rolesEl = $null + foreach ($child in $script:propsEl.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "DefaultRoles") { + $rolesEl = $child; break + } + } + if (-not $rolesEl) { Write-Error "No element found"; exit 1 } + + # Clear all existing children + while ($rolesEl.HasChildNodes) { + $rolesEl.RemoveChild($rolesEl.FirstChild) | Out-Null + } + + if ($items.Count -eq 0) { + $script:modifyCount++ + Info "Cleared DefaultRoles" + return + } + + $propsIndent = Get-ChildIndent $script:propsEl + $roleIndent = "$propsIndent`t" + + # Add closing whitespace + $closeWs = $script:xmlDoc.CreateWhitespace("`r`n$propsIndent") + $rolesEl.AppendChild($closeWs) | Out-Null + + foreach ($item in $items) { + $roleName = $item + if (-not $roleName.StartsWith("Role.")) { $roleName = "Role.$roleName" } + + $fragXml = "$roleName" + $nodes = Import-Fragment $fragXml + if ($nodes.Count -gt 0) { + Insert-BeforeElement $rolesEl $nodes[0] $null $roleIndent + } + } + + $script:modifyCount++ + Info "Set DefaultRoles: $($items.Count) roles" +} + +# --- Execute operations --- +$operations = @() +if ($DefinitionFile) { + if (-not [System.IO.Path]::IsPathRooted($DefinitionFile)) { + $DefinitionFile = Join-Path (Get-Location).Path $DefinitionFile + } + $jsonText = Get-Content -Raw -Encoding UTF8 $DefinitionFile + $ops = $jsonText | ConvertFrom-Json + if ($ops -is [System.Array]) { + foreach ($op in $ops) { $operations += $op } + } else { + $operations += $ops + } +} else { + $operations += @{ operation = $Operation; value = $Value } +} + +foreach ($op in $operations) { + $opName = if ($op.operation) { "$($op.operation)" } else { "$Operation" } + # Pass value through as-is (object or string); set-panels needs object form + $opValue = if ($null -ne $op.value) { $op.value } else { $Value } + $opValueStr = if ($opValue -is [string]) { $opValue } else { "$opValue" } + + switch ($opName) { + "modify-property" { Do-ModifyProperty $opValueStr } + "add-childObject" { Do-AddChildObject $opValueStr } + "remove-childObject" { Do-RemoveChildObject $opValueStr } + "add-defaultRole" { Do-AddDefaultRole $opValueStr } + "remove-defaultRole" { Do-RemoveDefaultRole $opValueStr } + "set-defaultRoles" { Do-SetDefaultRoles $opValueStr } + "set-panels" { Do-SetPanels $opValue } + "set-home-page" { Do-SetHomePage $opValue } + default { Write-Error "Unknown operation: $opName"; exit 1 } + } +} + +# --- Save --- +$settings = New-Object System.Xml.XmlWriterSettings +$settings.Encoding = New-Object System.Text.UTF8Encoding($true) +$settings.Indent = $false +$settings.NewLineHandling = [System.Xml.NewLineHandling]::None + +$memStream = New-Object System.IO.MemoryStream +$writer = [System.Xml.XmlWriter]::Create($memStream, $settings) +$script:xmlDoc.Save($writer) +$writer.Flush(); $writer.Close() + +$bytes = $memStream.ToArray() +$memStream.Close() +$text = [System.Text.Encoding]::UTF8.GetString($bytes) +if ($text.Length -gt 0 -and $text[0] -eq [char]0xFEFF) { $text = $text.Substring(1) } +$text = $text.Replace('encoding="utf-8"', 'encoding="UTF-8"') + +$utf8Bom = New-Object System.Text.UTF8Encoding($true) +[System.IO.File]::WriteAllText($resolvedPath, $text, $utf8Bom) +Info "Saved: $resolvedPath" + +# --- Auto-validate --- +if (-not $NoValidate) { + $validateScript = Join-Path (Join-Path $PSScriptRoot "..\..\cf-validate") "scripts\cf-validate.ps1" + $validateScript = [System.IO.Path]::GetFullPath($validateScript) + if (Test-Path $validateScript) { + Write-Host "" + Write-Host "--- Running cf-validate ---" + & powershell.exe -NoProfile -File $validateScript -ConfigPath $resolvedPath + } +} + +# --- Summary --- +Write-Host "" +Write-Host "=== cf-edit summary ===" +Write-Host " Configuration: $($script:objName)" +Write-Host " Added: $($script:addCount)" +Write-Host " Removed: $($script:removeCount)" +Write-Host " Modified: $($script:modifyCount)" +exit 0 diff --git a/.codex/skills/cf-edit/scripts/cf-edit.py b/.codex/skills/cf-edit/scripts/cf-edit.py new file mode 100644 index 00000000..ccd98c92 --- /dev/null +++ b/.codex/skills/cf-edit/scripts/cf-edit.py @@ -0,0 +1,822 @@ +#!/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() diff --git a/.codex/skills/cf-info/SKILL.md b/.codex/skills/cf-info/SKILL.md new file mode 100644 index 00000000..87830663 --- /dev/null +++ b/.codex/skills/cf-info/SKILL.md @@ -0,0 +1,54 @@ +--- +name: cf-info +description: Анализ структуры конфигурации 1С — свойства, состав, счётчики объектов. Используй для обзора конфигурации — какие объекты есть, сколько их, какие настройки +argument-hint: [-Mode overview|brief|full] [-Section home-page] +allowed-tools: + - Bash + - Read + - Glob +--- + +# /cf-info — Структура конфигурации 1С + +Читает Configuration.xml из выгрузки конфигурации и выводит компактное описание структуры. + +## Параметры и команда + +| Параметр | Описание | +|----------|----------| +| `ConfigPath` | Путь к Configuration.xml или каталогу выгрузки | +| `Mode` | Режим: `overview` (default), `brief`, `full` | +| `Section` | Drill-down по разделу (alias: `Name`). Сейчас: `home-page` | +| `Limit` / `Offset` | Пагинация (по умолчанию 150 строк) | +| `OutFile` | Записать результат в файл (UTF-8 BOM) | + +```powershell +powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/cf-info/scripts/cf-info.ps1" -ConfigPath "<путь>" +``` + +## Три режима + +| Режим | Что показывает | +|---|---| +| `overview` *(default)* | Заголовок + ключевые свойства + таблица счётчиков объектов по типам | +| `brief` | Одна строка: Имя — "Синоним" vВерсия \| N объектов \| совместимость | +| `full` | Все свойства по категориям + полный список ChildObjects + DefaultRoles + мобильные функциональности | + +## Примеры + +```powershell +# Обзор пустой конфигурации +... -ConfigPath src + +# Краткая сводка реальной конфигурации +... -ConfigPath src -Mode brief + +# Полная информация +... -ConfigPath src -Mode full + +# С пагинацией +... -ConfigPath src -Mode full -Limit 50 -Offset 100 + +# Drill-down: только начальная страница (раскладка форм с ролями) +... -ConfigPath src -Section home-page +``` diff --git a/.codex/skills/cf-info/scripts/cf-info.ps1 b/.codex/skills/cf-info/scripts/cf-info.ps1 new file mode 100644 index 00000000..d85df9aa --- /dev/null +++ b/.codex/skills/cf-info/scripts/cf-info.ps1 @@ -0,0 +1,579 @@ +# cf-info v1.2 — Compact summary of 1C configuration root +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( + [Parameter(Mandatory=$true)][Alias('Path')][string]$ConfigPath, + [ValidateSet("overview","brief","full")] + [string]$Mode = "overview", + [Alias('Name')] + [ValidateSet("home-page")] + [string]$Section, + [int]$Limit = 150, + [int]$Offset = 0, + [string]$OutFile +) + +$ErrorActionPreference = 'Stop' +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- Output helper (always collect, paginate at the end) --- +$script:lines = @() +function Out([string]$text) { $script:lines += $text } + +# --- Resolve path --- +if (-not [System.IO.Path]::IsPathRooted($ConfigPath)) { + $ConfigPath = Join-Path (Get-Location).Path $ConfigPath +} + +# Directory -> find Configuration.xml +if (Test-Path $ConfigPath -PathType Container) { + $candidate = Join-Path $ConfigPath "Configuration.xml" + if (Test-Path $candidate) { + $ConfigPath = $candidate + } else { + Write-Host "[ERROR] No Configuration.xml found in directory: $ConfigPath" + exit 1 + } +} + +if (-not (Test-Path $ConfigPath)) { + Write-Host "[ERROR] File not found: $ConfigPath" + exit 1 +} + +# --- Load XML --- +[xml]$xmlDoc = Get-Content -Path $ConfigPath -Encoding UTF8 +$ns = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable) +$ns.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") +$ns.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core") +$ns.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable") +$ns.AddNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance") +$ns.AddNamespace("xs", "http://www.w3.org/2001/XMLSchema") +$ns.AddNamespace("app", "http://v8.1c.ru/8.2/managed-application/core") + +$mdRoot = $xmlDoc.SelectSingleNode("/md:MetaDataObject", $ns) +if (-not $mdRoot) { + Write-Host "[ERROR] Not a valid 1C metadata XML file (no MetaDataObject root)" + exit 1 +} + +$cfgNode = $mdRoot.SelectSingleNode("md:Configuration", $ns) +if (-not $cfgNode) { + Write-Host "[ERROR] No element found" + exit 1 +} + +$version = $mdRoot.GetAttribute("version") +$propsNode = $cfgNode.SelectSingleNode("md:Properties", $ns) +$childObjNode = $cfgNode.SelectSingleNode("md:ChildObjects", $ns) + +# --- Helpers --- +function Get-MLText($node) { + if (-not $node) { return "" } + $item = $node.SelectSingleNode("v8:item/v8:content", $ns) + if ($item -and $item.InnerText) { return $item.InnerText } + return "" +} + +function Get-PropText([string]$propName) { + $n = $propsNode.SelectSingleNode("md:$propName", $ns) + if ($n -and $n.InnerText) { return $n.InnerText } + return "" +} + +function Get-PropML([string]$propName) { + $n = $propsNode.SelectSingleNode("md:$propName", $ns) + return (Get-MLText $n) +} + +# --- Type name maps (canonical order, 44 types) --- +$typeOrder = @( + "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" +) + +$typeRuNames = @{ + "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"="Сервисы интеграции" +} + +# --- Read panel layout (Ext/ClientApplicationInterface.xml) --- +$script:panelNames = @{ + "cbab57f2-a0f3-4f0a-89ea-4cb19570ab75" = "Открытых" + "b553047f-c9aa-4157-978d-448ecad24248" = "Разделов" + "13322b22-3960-4d68-93a6-fe2dd7f28ca3" = "Избранного" + "c933ac92-92cd-459d-81cc-e0c8a83ced99" = "История" + "b2735bd3-d822-4430-ba59-c9e869693b24" = "Функций" +} + +function Get-PanelsLayout { + $configDir = [System.IO.Path]::GetDirectoryName($ConfigPath) + $caiPath = Join-Path (Join-Path $configDir "Ext") "ClientApplicationInterface.xml" + if (-not (Test-Path $caiPath)) { return $null } + try { [xml]$caiDoc = Get-Content -Path $caiPath -Encoding UTF8 } catch { return $null } + if (-not $caiDoc.DocumentElement) { return $null } + $caiNs = New-Object System.Xml.XmlNamespaceManager($caiDoc.NameTable) + $caiNs.AddNamespace("ca", "http://v8.1c.ru/8.2/managed-application/core") + $layout = [ordered]@{ top=@(); left=@(); right=@(); bottom=@(); declared=@() } + foreach ($side in @("top","left","right","bottom")) { + foreach ($sideEl in $caiDoc.DocumentElement.SelectNodes("ca:$side", $caiNs)) { + $slot = @() + foreach ($u in $sideEl.SelectNodes(".//ca:panel/ca:uuid", $caiNs)) { + $key = $u.InnerText.Trim() + $nm = if ($script:panelNames.Contains($key)) { $script:panelNames[$key] } else { "?$key" } + $slot += $nm + } + if ($slot.Count -gt 0) { $layout[$side] += ,$slot } + } + } + foreach ($pd in $caiDoc.DocumentElement.SelectNodes("ca:panelDef", $caiNs)) { + $key = $pd.GetAttribute("id") + $nm = if ($script:panelNames.Contains($key)) { $script:panelNames[$key] } else { "?$key" } + $layout.declared += $nm + } + return $layout +} + +function Format-LayoutSlots($slots) { + # slots is array of arrays (each inner array = one side-tag's panels, may be 1+) + # Single inner array, single panel -> just name + # Single inner array, multiple panels -> "Стек(a, b)" + # Multiple inner arrays -> separate entries joined by " | " + if (-not $slots -or $slots.Count -eq 0) { return "" } + $parts = @() + foreach ($slot in $slots) { + if ($slot.Count -eq 1) { $parts += $slot[0] } + else { $parts += ("Стек(" + ($slot -join ", ") + ")") } + } + return ($parts -join " | ") +} + +$script:panelLayout = Get-PanelsLayout + +# --- Read home page layout (Ext/HomePageWorkArea.xml) --- +function Get-HomePageLayout { + $configDir = [System.IO.Path]::GetDirectoryName($ConfigPath) + $hpPath = Join-Path (Join-Path $configDir "Ext") "HomePageWorkArea.xml" + if (-not (Test-Path $hpPath)) { return $null } + try { [xml]$hpDoc = Get-Content -Path $hpPath -Encoding UTF8 } catch { return $null } + if (-not $hpDoc.DocumentElement) { return $null } + $hpNs = New-Object System.Xml.XmlNamespaceManager($hpDoc.NameTable) + $hpNs.AddNamespace("hp", "http://v8.1c.ru/8.3/xcf/extrnprops") + $hpNs.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable") + $result = [ordered]@{ template = ""; left = @(); right = @() } + $tmplNode = $hpDoc.DocumentElement.SelectSingleNode("hp:WorkingAreaTemplate", $hpNs) + if ($tmplNode) { $result.template = $tmplNode.InnerText.Trim() } + foreach ($colName in @("LeftColumn","RightColumn")) { + $colNode = $hpDoc.DocumentElement.SelectSingleNode("hp:$colName", $hpNs) + if (-not $colNode) { continue } + $items = @() + foreach ($item in $colNode.SelectNodes("hp:Item", $hpNs)) { + $f = $item.SelectSingleNode("hp:Form", $hpNs) + $h = $item.SelectSingleNode("hp:Height", $hpNs) + $visNode = $item.SelectSingleNode("hp:Visibility", $hpNs) + $common = $true + $roles = @() + if ($visNode) { + $cn = $visNode.SelectSingleNode("xr:Common", $hpNs) + if ($cn) { $common = ($cn.InnerText.Trim() -eq "true") } + foreach ($v in $visNode.SelectNodes("xr:Value", $hpNs)) { + $roles += @{ name = $v.GetAttribute("name"); value = ($v.InnerText.Trim() -eq "true") } + } + } + $items += [ordered]@{ + form = if ($f) { $f.InnerText.Trim() } else { "" } + height = if ($h) { [int]$h.InnerText.Trim() } else { 10 } + common = $common + roles = $roles + } + } + if ($colName -eq "LeftColumn") { $result.left = $items } else { $result.right = $items } + } + return $result +} + +$script:homePage = Get-HomePageLayout + +function Format-HomePageItem($it, [bool]$detailed) { + $badges = @() + $badges += "h=$($it.height)" + if (-not $it.common) { $badges += "скрыта" } + if ($it.roles.Count -gt 0) { + if ($detailed) { $badges += "роли: $($it.roles.Count)" } + else { $badges += "+$($it.roles.Count) ролей" } + } + $tail = if ($badges.Count -gt 0) { " (" + ($badges -join ", ") + ")" } else { "" } + return " $($it.form)$tail" +} + +# --- Count objects in ChildObjects --- +$objectCounts = [ordered]@{} +$totalObjects = 0 + +if ($childObjNode) { + foreach ($child in $childObjNode.ChildNodes) { + if ($child.NodeType -ne 'Element') { continue } + $typeName = $child.LocalName + if (-not $objectCounts.Contains($typeName)) { + $objectCounts[$typeName] = 0 + } + $objectCounts[$typeName] = $objectCounts[$typeName] + 1 + $totalObjects++ + } +} + +# --- Read key properties --- +$cfgName = Get-PropText "Name" +$cfgSynonym = Get-PropML "Synonym" +$cfgVersion = Get-PropText "Version" +$cfgVendor = Get-PropText "Vendor" +$cfgCompat = Get-PropText "CompatibilityMode" +$cfgExtCompat = Get-PropText "ConfigurationExtensionCompatibilityMode" +$cfgDefaultRun = Get-PropText "DefaultRunMode" +$cfgScript = Get-PropText "ScriptVariant" +$cfgDefaultLang = Get-PropText "DefaultLanguage" +$cfgDataLock = Get-PropText "DataLockControlMode" +$dash = [char]0x2014 +$cfgModality = Get-PropText "ModalityUseMode" +$cfgIntfCompat = Get-PropText "InterfaceCompatibilityMode" +$cfgAutoNum = Get-PropText "ObjectAutonumerationMode" +$cfgSyncCalls = Get-PropText "SynchronousPlatformExtensionAndAddInCallUseMode" +$cfgDbSpaces = Get-PropText "DatabaseTablespacesUseMode" +$cfgWindowMode = Get-PropText "MainClientApplicationWindowMode" + +# --- BRIEF mode --- +if ($Mode -eq "brief" -and -not $Section) { + $synPart = if ($cfgSynonym) { " $dash `"$cfgSynonym`"" } else { "" } + $verPart = if ($cfgVersion) { " v$cfgVersion" } else { "" } + $compatPart = if ($cfgCompat) { " | $cfgCompat" } else { "" } + Out "Конфигурация: ${cfgName}${synPart}${verPart} | $totalObjects объектов${compatPart}" +} + +# --- OVERVIEW mode --- +if ($Mode -eq "overview" -and -not $Section) { + $synPart = if ($cfgSynonym) { " $dash `"$cfgSynonym`"" } else { "" } + $verPart = if ($cfgVersion) { " v$cfgVersion" } else { "" } + Out "=== Конфигурация: ${cfgName}${synPart}${verPart} ===" + Out "" + + # Key properties + Out "Формат: $version" + if ($cfgVendor) { Out "Поставщик: $cfgVendor" } + if ($cfgVersion) { Out "Версия: $cfgVersion" } + Out "Совместимость: $cfgCompat" + Out "Режим запуска: $cfgDefaultRun" + Out "Язык скриптов: $cfgScript" + Out "Язык: $cfgDefaultLang" + Out "Блокировки: $cfgDataLock" + Out "Модальность: $cfgModality" + Out "Интерфейс: $cfgIntfCompat" + Out "" + + # Panel layout (if file exists) + if ($script:panelLayout) { + $hasPlaced = $false + foreach ($s in @("top","left","right","bottom")) { + if ($script:panelLayout[$s].Count -gt 0) { $hasPlaced = $true; break } + } + if ($hasPlaced) { + Out "--- Раскладка панелей ---" + foreach ($s in @("top","left","right","bottom")) { + if ($script:panelLayout[$s].Count -gt 0) { + Out " $($s.PadRight(7)) $(Format-LayoutSlots $script:panelLayout[$s])" + } + } + Out "" + } + } + + # Home page layout (brief summary) + if ($script:homePage) { + $ln = $script:homePage.left.Count + $rn = $script:homePage.right.Count + Out "--- Начальная страница ---" + Out " Шаблон: $($script:homePage.template)" + Out " LeftColumn: $ln, RightColumn: $rn (детали: -Section home-page)" + Out "" + } + + # Object counts table + Out "--- Состав ($totalObjects объектов) ---" + Out "" + $maxTypeLen = 0 + foreach ($typeName in $typeOrder) { + if ($objectCounts.Contains($typeName)) { + $ruName = $typeRuNames[$typeName] + if ($ruName.Length -gt $maxTypeLen) { $maxTypeLen = $ruName.Length } + } + } + if ($maxTypeLen -lt 10) { $maxTypeLen = 10 } + + foreach ($typeName in $typeOrder) { + if ($objectCounts.Contains($typeName)) { + $count = $objectCounts[$typeName] + $ruName = $typeRuNames[$typeName] + $padded = $ruName.PadRight($maxTypeLen) + Out " $padded $count" + } + } +} + +# --- Drill-down: -Section home-page --- +if ($Section -eq "home-page") { + if (-not $script:homePage) { + Out "Файл Ext/HomePageWorkArea.xml не найден" + } else { + Out "=== Начальная страница: $cfgName ===" + Out "" + Out "Шаблон: $($script:homePage.template)" + Out "" + foreach ($side in @(@("LeftColumn","left"), @("RightColumn","right"))) { + $items = $script:homePage[$side[1]] + $lbl = $side[0] + if ($items.Count -eq 0) { Out "${lbl}: —"; Out ""; continue } + Out "${lbl} ($($items.Count)):" + foreach ($it in $items) { + Out (Format-HomePageItem $it $true) + foreach ($r in $it.roles) { + $rval = if ($r.value) { "true" } else { "false" } + Out " $($r.name): $rval" + } + } + Out "" + } + } +} + +# --- FULL mode --- +if ($Mode -eq "full" -and -not $Section) { + $synPart = if ($cfgSynonym) { " $dash `"$cfgSynonym`"" } else { "" } + $verPart = if ($cfgVersion) { " v$cfgVersion" } else { "" } + Out "=== Конфигурация: ${cfgName}${synPart}${verPart} ===" + Out "" + + # --- Section: Identification --- + Out "--- Идентификация ---" + Out "UUID: $($cfgNode.GetAttribute('uuid'))" + Out "Имя: $cfgName" + if ($cfgSynonym) { Out "Синоним: $cfgSynonym" } + $cfgComment = Get-PropText "Comment" + if ($cfgComment) { Out "Комментарий: $cfgComment" } + $cfgPrefix = Get-PropText "NamePrefix" + if ($cfgPrefix) { Out "Префикс: $cfgPrefix" } + if ($cfgVendor) { Out "Поставщик: $cfgVendor" } + if ($cfgVersion) { Out "Версия: $cfgVersion" } + $cfgUpdateAddr = Get-PropText "UpdateCatalogAddress" + if ($cfgUpdateAddr) { Out "Каталог обн.: $cfgUpdateAddr" } + Out "" + + # --- Section: Modes --- + Out "--- Режимы работы ---" + Out "Формат: $version" + Out "Совместимость: $cfgCompat" + Out "Совм. расширений: $cfgExtCompat" + Out "Режим запуска: $cfgDefaultRun" + Out "Язык скриптов: $cfgScript" + Out "Блокировки: $cfgDataLock" + Out "Автонумерация: $cfgAutoNum" + Out "Модальность: $cfgModality" + Out "Синхр. вызовы: $cfgSyncCalls" + Out "Интерфейс: $cfgIntfCompat" + Out "Табл. пространства: $cfgDbSpaces" + Out "Режим окна: $cfgWindowMode" + Out "" + + # --- Section: Language, roles, purposes --- + Out "--- Назначение ---" + Out "Язык по умолч.: $cfgDefaultLang" + + # UsePurposes + $purposeNode = $propsNode.SelectSingleNode("md:UsePurposes", $ns) + if ($purposeNode) { + $purposes = @() + foreach ($val in $purposeNode.SelectNodes("v8:Value", $ns)) { + $purposes += $val.InnerText + } + if ($purposes.Count -gt 0) { Out "Назначения: $($purposes -join ', ')" } + } + + # DefaultRoles + $rolesNode = $propsNode.SelectSingleNode("md:DefaultRoles", $ns) + if ($rolesNode) { + $roles = @() + foreach ($item in $rolesNode.SelectNodes("xr:Item", $ns)) { + $roles += $item.InnerText + } + if ($roles.Count -gt 0) { + Out "Роли по умолч.: $($roles.Count)" + foreach ($r in $roles) { Out " - $r" } + } + } + + # Booleans + $useMF = Get-PropText "UseManagedFormInOrdinaryApplication" + $useOF = Get-PropText "UseOrdinaryFormInManagedApplication" + Out "Управл.формы в обычн.: $useMF" + Out "Обычн.формы в управл.: $useOF" + Out "" + + # --- Section: Panel layout --- + if ($script:panelLayout) { + Out "--- Раскладка панелей ---" + foreach ($s in @("top","left","right","bottom")) { + $slots = $script:panelLayout[$s] + if ($slots.Count -gt 0) { + Out " $($s.PadRight(7)) $(Format-LayoutSlots $slots)" + } else { + Out " $($s.PadRight(7)) —" + } + } + if ($script:panelLayout.declared.Count -gt 0) { + Out " объявлено: $($script:panelLayout.declared -join ', ')" + } + Out "" + } + + # --- Section: Home page (brief summary) --- + if ($script:homePage) { + $ln = $script:homePage.left.Count + $rn = $script:homePage.right.Count + Out "--- Начальная страница ---" + Out " Шаблон: $($script:homePage.template)" + Out " LeftColumn: $ln, RightColumn: $rn (детали: -Section home-page)" + Out "" + } + + # --- Section: Storages & default forms --- + Out "--- Хранилища и формы по умолчанию ---" + $storageProps = @("CommonSettingsStorage","ReportsUserSettingsStorage","ReportsVariantsStorage","FormDataSettingsStorage","DynamicListsUserSettingsStorage","URLExternalDataStorage") + foreach ($sp in $storageProps) { + $val = Get-PropText $sp + if ($val) { Out " ${sp}: $val" } + } + $formProps = @("DefaultReportForm","DefaultReportVariantForm","DefaultReportSettingsForm","DefaultReportAppearanceTemplate","DefaultDynamicListSettingsForm","DefaultSearchForm","DefaultDataHistoryChangeHistoryForm","DefaultDataHistoryVersionDataForm","DefaultDataHistoryVersionDifferencesForm","DefaultCollaborationSystemUsersChoiceForm","DefaultConstantsForm","DefaultInterface","DefaultStyle") + foreach ($fp in $formProps) { + $val = Get-PropText $fp + if ($val) { Out " ${fp}: $val" } + } + Out "" + + # --- Section: Info --- + $cfgBrief = Get-PropML "BriefInformation" + $cfgDetail = Get-PropML "DetailedInformation" + $cfgCopyright = Get-PropML "Copyright" + $cfgVendorAddr = Get-PropML "VendorInformationAddress" + $cfgInfoAddr = Get-PropML "ConfigurationInformationAddress" + if ($cfgBrief -or $cfgDetail -or $cfgCopyright -or $cfgVendorAddr -or $cfgInfoAddr) { + Out "--- Информация ---" + if ($cfgBrief) { Out "Краткая: $cfgBrief" } + if ($cfgDetail) { Out "Подробная: $cfgDetail" } + if ($cfgCopyright) { Out "Copyright: $cfgCopyright" } + if ($cfgVendorAddr) { Out "Сайт поставщика: $cfgVendorAddr" } + if ($cfgInfoAddr) { Out "Адрес информ.: $cfgInfoAddr" } + Out "" + } + + # --- Section: Mobile functionalities --- + $mobileFunc = $propsNode.SelectSingleNode("md:UsedMobileApplicationFunctionalities", $ns) + if ($mobileFunc) { + $enabledFuncs = @() + $disabledFuncs = @() + foreach ($func in $mobileFunc.SelectNodes("app:functionality", $ns)) { + $fName = $func.SelectSingleNode("app:functionality", $ns) + $fUse = $func.SelectSingleNode("app:use", $ns) + if ($fName -and $fUse) { + if ($fUse.InnerText -eq "true") { + $enabledFuncs += $fName.InnerText + } else { + $disabledFuncs += $fName.InnerText + } + } + } + $totalFunc = $enabledFuncs.Count + $disabledFuncs.Count + Out "--- Мобильные функциональности ($totalFunc, включено: $($enabledFuncs.Count)) ---" + if ($enabledFuncs.Count -gt 0) { + foreach ($f in $enabledFuncs) { Out " [+] $f" } + } + foreach ($f in $disabledFuncs) { Out " [-] $f" } + Out "" + } + + # --- Section: InternalInfo --- + $internalInfo = $cfgNode.SelectSingleNode("md:InternalInfo", $ns) + if ($internalInfo) { + $contained = $internalInfo.SelectNodes("xr:ContainedObject", $ns) + Out "--- InternalInfo ($($contained.Count) ContainedObject) ---" + foreach ($co in $contained) { + $classId = $co.SelectSingleNode("xr:ClassId", $ns).InnerText + $objectId = $co.SelectSingleNode("xr:ObjectId", $ns).InnerText + Out " $classId -> $objectId" + } + Out "" + } + + # --- Section: ChildObjects (full list) --- + Out "--- Состав ($totalObjects объектов) ---" + Out "" + + foreach ($typeName in $typeOrder) { + if (-not $objectCounts.Contains($typeName)) { continue } + $count = $objectCounts[$typeName] + $ruName = $typeRuNames[$typeName] + Out " $ruName ($typeName): $count" + + # Collect names for this type + $names = @() + foreach ($child in $childObjNode.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq $typeName) { + $names += $child.InnerText + } + } + foreach ($n in $names) { Out " $n" } + } +} + +# --- Pagination and output --- +$total = $script:lines.Count +if ($Offset -gt 0 -or $Limit -lt $total) { + $start = [Math]::Min($Offset, $total) + $end = [Math]::Min($start + $Limit, $total) + $page = $script:lines[$start..($end - 1)] + $result = ($page -join "`n") + if ($end -lt $total) { + $result += "`n`n... ($end of $total lines, use -Offset $end to continue)" + } +} else { + $result = ($script:lines -join "`n") +} + +Write-Host $result + +if ($OutFile) { + $utf8Bom = New-Object System.Text.UTF8Encoding $true + [System.IO.File]::WriteAllText($OutFile, $result, $utf8Bom) + Write-Host "`nWritten to: $OutFile" +} diff --git a/.codex/skills/cf-info/scripts/cf-info.py b/.codex/skills/cf-info/scripts/cf-info.py new file mode 100644 index 00000000..bd781e4d --- /dev/null +++ b/.codex/skills/cf-info/scripts/cf-info.py @@ -0,0 +1,562 @@ +#!/usr/bin/env python3 +# cf-info v1.2 — 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 + +sys.stdout.reconfigure(encoding="utf-8") +sys.stderr.reconfigure(encoding="utf-8") + +# --- Argument parsing --- +parser = argparse.ArgumentParser(description="Analyze 1C configuration structure", allow_abbrev=False) +parser.add_argument("-ConfigPath", "-Path", 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("-Section", "-Name", choices=["home-page"], default=None, help="Drill-down section (alias: -Name)") +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": "Сервисы интеграции", +} + +# --- Read panel layout (Ext/ClientApplicationInterface.xml) --- +PANEL_NAMES = { + "cbab57f2-a0f3-4f0a-89ea-4cb19570ab75": "Открытых", + "b553047f-c9aa-4157-978d-448ecad24248": "Разделов", + "13322b22-3960-4d68-93a6-fe2dd7f28ca3": "Избранного", + "c933ac92-92cd-459d-81cc-e0c8a83ced99": "История", + "b2735bd3-d822-4430-ba59-c9e869693b24": "Функций", +} +CAI_NS = "http://v8.1c.ru/8.2/managed-application/core" + +def get_panels_layout(): + cfg_dir = os.path.dirname(config_path) + cai_path = os.path.join(cfg_dir, "Ext", "ClientApplicationInterface.xml") + if not os.path.isfile(cai_path): + return None + try: + cai_tree = etree.parse(cai_path) + except Exception: + return None + cai_root = cai_tree.getroot() + layout = {"top": [], "left": [], "right": [], "bottom": [], "declared": []} + for side in ("top", "left", "right", "bottom"): + for side_el in cai_root.findall(f"{{{CAI_NS}}}{side}"): + slot = [] + for u in side_el.iter(f"{{{CAI_NS}}}uuid"): + key = (u.text or "").strip() + slot.append(PANEL_NAMES.get(key, f"?{key}")) + if slot: + layout[side].append(slot) + for pd in cai_root.findall(f"{{{CAI_NS}}}panelDef"): + key = pd.get("id", "") + layout["declared"].append(PANEL_NAMES.get(key, f"?{key}")) + return layout + +def format_layout_slots(slots): + if not slots: + return "" + parts = [] + for slot in slots: + if len(slot) == 1: + parts.append(slot[0]) + else: + parts.append("Стек(" + ", ".join(slot) + ")") + return " | ".join(parts) + +panel_layout = get_panels_layout() + +# --- Read home page layout (Ext/HomePageWorkArea.xml) --- +HP_NS = "http://v8.1c.ru/8.3/xcf/extrnprops" +XR_NS_HP = "http://v8.1c.ru/8.3/xcf/readable" + +def get_home_page_layout(): + cfg_dir = os.path.dirname(config_path) + hp_path = os.path.join(cfg_dir, "Ext", "HomePageWorkArea.xml") + if not os.path.isfile(hp_path): + return None + try: + hp_tree = etree.parse(hp_path) + except Exception: + return None + hp_root = hp_tree.getroot() + result = {"template": "", "left": [], "right": []} + tn = hp_root.find(f"{{{HP_NS}}}WorkingAreaTemplate") + if tn is not None and tn.text: + result["template"] = tn.text.strip() + for col_name, key in (("LeftColumn", "left"), ("RightColumn", "right")): + col = hp_root.find(f"{{{HP_NS}}}{col_name}") + if col is None: + continue + items = [] + for it in col.findall(f"{{{HP_NS}}}Item"): + f = it.find(f"{{{HP_NS}}}Form") + h = it.find(f"{{{HP_NS}}}Height") + vis = it.find(f"{{{HP_NS}}}Visibility") + common = True + roles = [] + if vis is not None: + cn = vis.find(f"{{{XR_NS_HP}}}Common") + if cn is not None and cn.text: + common = cn.text.strip() == "true" + for v in vis.findall(f"{{{XR_NS_HP}}}Value"): + roles.append({"name": v.get("name", ""), "value": (v.text or "").strip() == "true"}) + items.append({ + "form": (f.text or "").strip() if f is not None else "", + "height": int((h.text or "10").strip()) if h is not None else 10, + "common": common, + "roles": roles, + }) + result[key] = items + return result + +home_page = get_home_page_layout() + +def format_home_page_item(it, detailed): + badges = [f"h={it['height']}"] + if not it["common"]: + badges.append("скрыта") + if it["roles"]: + badges.append(f"роли: {len(it['roles'])}" if detailed else f"+{len(it['roles'])} ролей") + tail = f" ({', '.join(badges)})" if badges else "" + return f" {it['form']}{tail}" + +# --- 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" and not args.Section: + 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" and not args.Section: + 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() + + if panel_layout and any(panel_layout[s] for s in ("top", "left", "right", "bottom")): + out("--- Раскладка панелей ---") + for s in ("top", "left", "right", "bottom"): + if panel_layout[s]: + out(f" {s.ljust(7)} {format_layout_slots(panel_layout[s])}") + out() + + # Home page (brief summary) + if home_page: + out("--- Начальная страница ---") + out(f" Шаблон: {home_page['template']}") + out(f" LeftColumn: {len(home_page['left'])}, RightColumn: {len(home_page['right'])} (детали: -Section home-page)") + 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 --- +# --- Drill-down: -Section home-page --- +if args.Section == "home-page": + if not home_page: + out("Файл Ext/HomePageWorkArea.xml не найден") + else: + out(f"=== Начальная страница: {cfg_name} ===") + out() + out(f"Шаблон: {home_page['template']}") + out() + for col_lbl, col_key in (("LeftColumn", "left"), ("RightColumn", "right")): + items = home_page[col_key] + if not items: + out(f"{col_lbl}: —") + out() + continue + out(f"{col_lbl} ({len(items)}):") + for it in items: + out(format_home_page_item(it, True)) + for r in it["roles"]: + rval = "true" if r["value"] else "false" + out(f" {r['name']}: {rval}") + out() + +if args.Mode == "full" and not args.Section: + 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: Panel layout --- + if panel_layout: + out("--- Раскладка панелей ---") + for s in ("top", "left", "right", "bottom"): + slots = panel_layout[s] + if slots: + out(f" {s.ljust(7)} {format_layout_slots(slots)}") + else: + out(f" {s.ljust(7)} —") + if panel_layout["declared"]: + out(f" объявлено: {', '.join(panel_layout['declared'])}") + out() + + # --- Section: Home page (brief summary) --- + if home_page: + out("--- Начальная страница ---") + out(f" Шаблон: {home_page['template']}") + out(f" LeftColumn: {len(home_page['left'])}, RightColumn: {len(home_page['right'])} (детали: -Section home-page)") + 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/.codex/skills/cf-init/SKILL.md b/.codex/skills/cf-init/SKILL.md new file mode 100644 index 00000000..b7cf4cbf --- /dev/null +++ b/.codex/skills/cf-init/SKILL.md @@ -0,0 +1,49 @@ +--- +name: cf-init +description: Создать пустую конфигурацию 1С (scaffold XML-исходников). Используй когда нужно начать новую конфигурацию с нуля +argument-hint: [-Synonym ] [-OutputDir src] +allowed-tools: + - Bash + - Read + - Glob +--- + +# /cf-init — Создание пустой конфигурации 1С + +Создаёт scaffold исходников пустой конфигурации 1С: `Configuration.xml`, `Languages/Русский.xml`. + +## Параметры и команда + +| Параметр | Описание | +|----------|----------| +| `Name` | Имя конфигурации (обязат.) | +| `Synonym` | Синоним (= Name если не указан) | +| `OutputDir` | Каталог для создания (default: `src`) | +| `Version` | Версия конфигурации | +| `Vendor` | Поставщик | +| `CompatibilityMode` | Режим совместимости (default: `Version8_3_24`) | + +```powershell +powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/cf-init/scripts/cf-init.ps1" -Name "МояКонфигурация" +``` + +## Примеры + +```powershell +# Базовая конфигурация +... -Name МояКонфигурация -Synonym "Моя конфигурация" -OutputDir test-tmp/cf + +# С версией и поставщиком +... -Name TestCfg -Synonym "Тестовая" -Version "1.0.0.1" -Vendor "Фирма 1С" -OutputDir test-tmp/cf2 + +# Другой режим совместимости +... -Name TestCfg -CompatibilityMode Version8_3_27 -OutputDir test-tmp/cf3 +``` + +## Верификация + +``` +/cf-init TestConfig -OutputDir test-tmp/cf +/cf-info test-tmp/cf — проверить созданное +/cf-validate test-tmp/cf — валидировать +``` diff --git a/.codex/skills/cf-init/scripts/cf-init.ps1 b/.codex/skills/cf-init/scripts/cf-init.ps1 new file mode 100644 index 00000000..69603dca --- /dev/null +++ b/.codex/skills/cf-init/scripts/cf-init.ps1 @@ -0,0 +1,249 @@ +# cf-init v1.2 — Create empty 1C configuration scaffold +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( + [Parameter(Mandatory)] + [string]$Name, + [string]$Synonym = $Name, + [string]$OutputDir = "src", + [string]$Version, + [string]$Vendor, + [string]$CompatibilityMode = "Version8_3_24" +) + +$ErrorActionPreference = "Stop" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- Resolve output dir --- +if (-not [System.IO.Path]::IsPathRooted($OutputDir)) { + $OutputDir = Join-Path (Get-Location).Path $OutputDir +} + +# --- Check existing --- +$cfgFile = Join-Path $OutputDir "Configuration.xml" +if (Test-Path $cfgFile) { + Write-Error "Configuration.xml already exists: $cfgFile" + exit 1 +} + +# --- Generate UUIDs --- +$uuidCfg = [guid]::NewGuid().ToString() +$uuidLang = [guid]::NewGuid().ToString() +# 7 ContainedObject ObjectIds +$co1 = [guid]::NewGuid().ToString() +$co2 = [guid]::NewGuid().ToString() +$co3 = [guid]::NewGuid().ToString() +$co4 = [guid]::NewGuid().ToString() +$co5 = [guid]::NewGuid().ToString() +$co6 = [guid]::NewGuid().ToString() +$co7 = [guid]::NewGuid().ToString() + +# --- Mobile functionalities --- +$mobileFuncs = @( + @("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") +) + +$mobileXml = "" +foreach ($mf in $mobileFuncs) { + $mobileXml += "`r`n`t`t`t`t`r`n`t`t`t`t`t$($mf[0])`r`n`t`t`t`t`t$($mf[1])`r`n`t`t`t`t" +} + +# --- Synonym XML --- +$synonymXml = "" +if ($Synonym) { + $synonymXml = "`r`n`t`t`t`t`r`n`t`t`t`t`tru`r`n`t`t`t`t`t$([System.Security.SecurityElement]::Escape($Synonym))`r`n`t`t`t`t`r`n`t`t`t" +} + +# --- Optional properties --- +$vendorXml = if ($Vendor) { [System.Security.SecurityElement]::Escape($Vendor) } else { "" } +$versionXml = if ($Version) { [System.Security.SecurityElement]::Escape($Version) } else { "" } + +# --- Configuration.xml --- +$cfgXml = @" + + + + + + 9cd510cd-abfc-11d4-9434-004095e12fc7 + $co1 + + + 9fcd25a0-4822-11d4-9414-008048da11f9 + $co2 + + + e3687481-0a87-462c-a166-9f34594f9bba + $co3 + + + 9de14907-ec23-4a07-96f0-85521cb6b53b + $co4 + + + 51f2d5d8-ea4d-4064-8892-82951750031e + $co5 + + + e68182ea-4237-4383-967f-90c1e3370bc7 + $co6 + + + fb282519-d103-4dd3-bc12-cb271d631dfc + $co7 + + + + $([System.Security.SecurityElement]::Escape($Name)) + $synonymXml + + + $CompatibilityMode + ManagedApplication + + PlatformApplication + + Russian + + $vendorXml + $versionXml + + false + false + false + + + + + + + + + + + + + + + + + + + + $mobileXml + + + + + Normal + + + Language.Русский + + + + + + Managed + NotAutoFree + DontUse + DontUse + TaxiEnableVersion8_2 + DontUse + $CompatibilityMode + + + + Русский + + + +"@ + +# --- Languages/Русский.xml --- +$langXml = @" + + + + + Русский + + + ru + Русский + + + + ru + + + +"@ + +# --- Ext/ClientApplicationInterface.xml (default ERP-style panel layout) --- +# Open panel on top, Sections panel on left; Functions/Favorites/History declared +# via panelDef but not placed by default. Without this file the web client renders +# section icons without labels (icon-only mode). +$openPanelInst = [guid]::NewGuid().ToString() +$sectionsPanelInst = [guid]::NewGuid().ToString() +$caiXml = @" + + + + + cbab57f2-a0f3-4f0a-89ea-4cb19570ab75 + + + + + b553047f-c9aa-4157-978d-448ecad24248 + + + + + + + + +"@ + +# --- Create directories --- +if (-not (Test-Path $OutputDir)) { + New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null +} +$langDir = Join-Path $OutputDir "Languages" +if (-not (Test-Path $langDir)) { + New-Item -ItemType Directory -Path $langDir -Force | Out-Null +} +$extDir = Join-Path $OutputDir "Ext" +if (-not (Test-Path $extDir)) { + New-Item -ItemType Directory -Path $extDir -Force | Out-Null +} + +# --- Write files with UTF-8 BOM --- +$enc = New-Object System.Text.UTF8Encoding($true) + +[System.IO.File]::WriteAllText($cfgFile, $cfgXml, $enc) +$langFile = Join-Path $langDir "Русский.xml" +[System.IO.File]::WriteAllText($langFile, $langXml, $enc) +$caiFile = Join-Path $extDir "ClientApplicationInterface.xml" +[System.IO.File]::WriteAllText($caiFile, $caiXml, $enc) + +# --- Output --- +Write-Host "[OK] Создана конфигурация: $Name" +Write-Host " Каталог: $OutputDir" +Write-Host " Configuration.xml: $cfgFile" +Write-Host " Languages: $langFile" +Write-Host " Ext/CAI: $caiFile" diff --git a/.codex/skills/cf-init/scripts/cf-init.py b/.codex/skills/cf-init/scripts/cf-init.py new file mode 100644 index 00000000..d9339251 --- /dev/null +++ b/.codex/skills/cf-init/scripts/cf-init.py @@ -0,0 +1,233 @@ +#!/usr/bin/env python3 +# cf-init v1.2 — 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(): + sys.stdout.reconfigure(encoding="utf-8") + sys.stderr.reconfigure(encoding="utf-8") + 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\tTaxiEnableVersion8_2 +\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 +''' + + # --- Ext/ClientApplicationInterface.xml (default ERP-style panel layout) --- + # Open panel on top, Sections panel on left; Functions/Favorites/History declared + # via panelDef but not placed by default. Without this file the web client renders + # section icons without labels (icon-only mode). + open_panel_inst = new_uuid() + sections_panel_inst = new_uuid() + cai_xml = f''' + +\t +\t\t +\t\t\tcbab57f2-a0f3-4f0a-89ea-4cb19570ab75 +\t\t +\t +\t +\t\t +\t\t\tb553047f-c9aa-4157-978d-448ecad24248 +\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) + ext_dir = os.path.join(output_dir, "Ext") + os.makedirs(ext_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) + cai_file = os.path.join(ext_dir, "ClientApplicationInterface.xml") + write_utf8_bom(cai_file, cai_xml) + + print(f"[OK] Создана конфигурация: {name}") + print(f" Каталог: {output_dir}") + print(f" Configuration.xml: {cfg_file}") + print(f" Languages: {lang_file}") + print(f" Ext/CAI: {cai_file}") + +if __name__ == '__main__': + main() diff --git a/.codex/skills/cf-validate/SKILL.md b/.codex/skills/cf-validate/SKILL.md new file mode 100644 index 00000000..587e087b --- /dev/null +++ b/.codex/skills/cf-validate/SKILL.md @@ -0,0 +1,29 @@ +--- +name: cf-validate +description: Валидация конфигурации 1С. Используй после создания или модификации конфигурации для проверки корректности +argument-hint: [-Detailed] [-MaxErrors 30] +allowed-tools: + - Bash + - Read + - Glob +--- + +# /cf-validate — валидация конфигурации 1С + +Проверяет Configuration.xml на структурные ошибки: XML well-formedness, InternalInfo, свойства, enum-значения, ChildObjects, DefaultLanguage, файлы языков, каталоги объектов. + +## Параметры + +| Параметр | Обяз. | Умолч. | Описание | +|------------|:-----:|---------|-------------------------------------------------| +| ConfigPath | да | — | Путь к Configuration.xml или каталогу выгрузки | +| Detailed | нет | — | Подробный вывод (все проверки, включая успешные) | +| MaxErrors | нет | 30 | Остановиться после N ошибок | +| OutFile | нет | — | Записать результат в файл (UTF-8 BOM) | + +## Команда + +```powershell +powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/cf-validate/scripts/cf-validate.ps1" -ConfigPath "upload/cfempty" +powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/cf-validate/scripts/cf-validate.ps1" -ConfigPath "upload/cfempty/Configuration.xml" +``` diff --git a/.codex/skills/cf-validate/scripts/cf-validate.ps1 b/.codex/skills/cf-validate/scripts/cf-validate.ps1 new file mode 100644 index 00000000..f7c071a4 --- /dev/null +++ b/.codex/skills/cf-validate/scripts/cf-validate.ps1 @@ -0,0 +1,611 @@ +# cf-validate v1.3 — Validate 1C configuration root structure +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( + [Parameter(Mandatory)] + [Alias('Path')] + [string]$ConfigPath, + + [switch]$Detailed, + + [int]$MaxErrors = 30, + + [string]$OutFile +) + +$ErrorActionPreference = "Stop" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- Resolve path --- +if (-not [System.IO.Path]::IsPathRooted($ConfigPath)) { + $ConfigPath = Join-Path (Get-Location).Path $ConfigPath +} + +if (Test-Path $ConfigPath -PathType Container) { + $candidate = Join-Path $ConfigPath "Configuration.xml" + if (Test-Path $candidate) { + $ConfigPath = $candidate + } else { + Write-Host "[ERROR] No Configuration.xml found in directory: $ConfigPath" + exit 1 + } +} + +if (-not (Test-Path $ConfigPath)) { + Write-Host "[ERROR] File not found: $ConfigPath" + exit 1 +} + +$resolvedPath = (Resolve-Path $ConfigPath).Path +$configDir = Split-Path $resolvedPath -Parent + +# --- Output infrastructure --- +$script:errors = 0 +$script:warnings = 0 +$script:okCount = 0 +$script:stopped = $false +$script:output = New-Object System.Text.StringBuilder 8192 + +function Out-Line { + param([string]$msg) + $script:output.AppendLine($msg) | Out-Null +} + +function Report-OK { + param([string]$msg) + $script:okCount++ + if ($Detailed) { Out-Line "[OK] $msg" } +} + +function Report-Error { + param([string]$msg) + $script:errors++ + Out-Line "[ERROR] $msg" + if ($script:errors -ge $MaxErrors) { + $script:stopped = $true + } +} + +function Report-Warn { + param([string]$msg) + $script:warnings++ + Out-Line "[WARN] $msg" +} + +$finalize = { + $checks = $script:okCount + $script:errors + $script:warnings + if ($script:errors -eq 0 -and $script:warnings -eq 0 -and -not $Detailed) { + $result = "=== Validation OK: Configuration.$objName ($checks checks) ===" + } else { + Out-Line "" + Out-Line "=== Result: $($script:errors) errors, $($script:warnings) warnings ($checks checks) ===" + $result = $script:output.ToString() + } + Write-Host $result + + if ($OutFile) { + $utf8Bom = New-Object System.Text.UTF8Encoding $true + [System.IO.File]::WriteAllText($OutFile, $result, $utf8Bom) + Write-Host "Written to: $OutFile" + } +} + +# --- Reference tables --- +$guidPattern = '^[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}$' +$identPattern = '^[A-Za-z\u0410-\u042F\u0401\u0430-\u044F\u0451_][A-Za-z0-9\u0410-\u042F\u0401\u0430-\u044F\u0451_]*$' + +# 7 fixed ClassIds for Configuration +$validClassIds = @( + "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 +$childObjectTypes = @( + "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 +$childTypeDirMap = @{ + "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 +$validEnumValues = @{ + "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","Version8_5_1") + "DefaultRunMode" = @("ManagedApplication","OrdinaryApplication","Auto") + "ScriptVariant" = @("Russian","English") + "DataLockControlMode" = @("Automatic","Managed","AutomaticAndManaged") + "ObjectAutonumerationMode" = @("NotAutoFree","AutoFree") + "ModalityUseMode" = @("DontUse","Use","UseWithWarnings") + "SynchronousPlatformExtensionAndAddInCallUseMode" = @("DontUse","Use","UseWithWarnings") + "InterfaceCompatibilityMode" = @("Version8_2","Version8_2EnableTaxi","Taxi","TaxiEnableVersion8_2","TaxiEnableVersion8_5","Version8_5EnableTaxi","Version8_5") + "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","Version8_5_1") +} + +# --- 1. Parse XML --- +Out-Line "" + +$xmlDoc = $null +try { + $xmlDoc = New-Object System.Xml.XmlDocument + $xmlDoc.PreserveWhitespace = $false + $xmlDoc.Load($resolvedPath) +} catch { + Out-Line "=== Validation: Configuration (parse failed) ===" + Out-Line "" + Report-Error "1. XML parse failed: $($_.Exception.Message)" + & $finalize + exit 1 +} + +# --- Register namespaces --- +$ns = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable) +$ns.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") +$ns.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core") +$ns.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable") +$ns.AddNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance") +$ns.AddNamespace("xs", "http://www.w3.org/2001/XMLSchema") +$ns.AddNamespace("app", "http://v8.1c.ru/8.2/managed-application/core") + +$root = $xmlDoc.DocumentElement + +# --- Check 1: Root structure --- +$check1Ok = $true +$expectedNs = "http://v8.1c.ru/8.3/MDClasses" + +if ($root.LocalName -ne "MetaDataObject") { + Report-Error "1. Root element is '$($root.LocalName)', expected 'MetaDataObject'" + & $finalize + exit 1 +} + +if ($root.NamespaceURI -ne $expectedNs) { + Report-Error "1. Root namespace is '$($root.NamespaceURI)', expected '$expectedNs'" + $check1Ok = $false +} + +$version = $root.GetAttribute("version") +if (-not $version) { + Report-Warn "1. Missing version attribute on MetaDataObject" +} elseif ($version -ne "2.17" -and $version -ne "2.20" -and $version -ne "2.21") { + Report-Warn "1. Unusual version '$version' (expected 2.17, 2.20 or 2.21)" +} + +# Must have Configuration child +$cfgNode = $null +foreach ($child in $root.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Configuration" -and $child.NamespaceURI -eq $expectedNs) { + $cfgNode = $child; break + } +} + +if (-not $cfgNode) { + Report-Error "1. No element found inside MetaDataObject" + & $finalize + exit 1 +} + +# UUID +$cfgUuid = $cfgNode.GetAttribute("uuid") +if (-not $cfgUuid) { + Report-Error "1. Missing uuid on " + $check1Ok = $false +} elseif ($cfgUuid -notmatch $guidPattern) { + Report-Error "1. Invalid uuid '$cfgUuid' on " + $check1Ok = $false +} + +# Get name early for header +$propsNode = $cfgNode.SelectSingleNode("md:Properties", $ns) +$nameNode = if ($propsNode) { $propsNode.SelectSingleNode("md:Name", $ns) } else { $null } +$objName = if ($nameNode -and $nameNode.InnerText) { $nameNode.InnerText } else { "(unknown)" } + +$script:output.Insert(0, "=== Validation: Configuration.$objName ===$([Environment]::NewLine)") | Out-Null + +if ($check1Ok) { + Report-OK "1. Root structure: MetaDataObject/Configuration, version $version" +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 2: InternalInfo --- +$internalInfo = $cfgNode.SelectSingleNode("md:InternalInfo", $ns) +$check2Ok = $true + +if (-not $internalInfo) { + Report-Error "2. InternalInfo: missing" +} else { + $contained = $internalInfo.SelectNodes("xr:ContainedObject", $ns) + if ($contained.Count -ne 7) { + Report-Warn "2. InternalInfo: expected 7 ContainedObject, found $($contained.Count)" + } + + $foundClassIds = @{} + foreach ($co in $contained) { + $classId = $co.SelectSingleNode("xr:ClassId", $ns) + $objectId = $co.SelectSingleNode("xr:ObjectId", $ns) + + if (-not $classId -or -not $classId.InnerText) { + Report-Error "2. ContainedObject missing ClassId" + $check2Ok = $false + continue + } + + $cid = $classId.InnerText + if ($validClassIds -notcontains $cid) { + Report-Error "2. Unknown ClassId: $cid" + $check2Ok = $false + } + + if ($foundClassIds.ContainsKey($cid)) { + Report-Error "2. Duplicate ClassId: $cid" + $check2Ok = $false + } + $foundClassIds[$cid] = $true + + if (-not $objectId -or -not $objectId.InnerText) { + Report-Error "2. ContainedObject missing ObjectId for ClassId $cid" + $check2Ok = $false + } elseif ($objectId.InnerText -notmatch $guidPattern) { + Report-Error "2. Invalid ObjectId '$($objectId.InnerText)' for ClassId $cid" + $check2Ok = $false + } + } + + # Check missing ClassIds + $missingIds = @($validClassIds | Where-Object { -not $foundClassIds.ContainsKey($_) }) + if ($missingIds.Count -gt 0) { + Report-Warn "2. Missing ClassIds: $($missingIds.Count) of 7" + } + + if ($check2Ok) { + Report-OK "2. InternalInfo: $($contained.Count) ContainedObject, all ClassIds valid" + } +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 3: Properties — Name, Synonym, DefaultLanguage, DefaultRunMode --- +if (-not $propsNode) { + Report-Error "3. Properties block missing" +} else { + $check3Ok = $true + + # Name + if (-not $nameNode -or -not $nameNode.InnerText) { + Report-Error "3. Properties: Name is missing or empty" + $check3Ok = $false + } else { + $nameVal = $nameNode.InnerText + if ($nameVal -notmatch $identPattern) { + Report-Error "3. Properties: Name '$nameVal' is not a valid 1C identifier" + $check3Ok = $false + } + } + + # Synonym + $synNode = $propsNode.SelectSingleNode("md:Synonym", $ns) + $synPresent = $false + if ($synNode) { + $synItem = $synNode.SelectSingleNode("v8:item", $ns) + if ($synItem) { + $synContent = $synItem.SelectSingleNode("v8:content", $ns) + if ($synContent -and $synContent.InnerText) { $synPresent = $true } + } + } + + # DefaultLanguage + $defLangNode = $propsNode.SelectSingleNode("md:DefaultLanguage", $ns) + $defLang = if ($defLangNode -and $defLangNode.InnerText) { $defLangNode.InnerText } else { "" } + if (-not $defLang) { + Report-Error "3. Properties: DefaultLanguage is missing or empty" + $check3Ok = $false + } + + # DefaultRunMode + $defRunNode = $propsNode.SelectSingleNode("md:DefaultRunMode", $ns) + if (-not $defRunNode -or -not $defRunNode.InnerText) { + Report-Warn "3. Properties: DefaultRunMode is missing or empty" + } + + if ($check3Ok) { + $synInfo = if ($synPresent) { "Synonym present" } else { "no Synonym" } + Report-OK "3. Properties: Name=`"$objName`", $synInfo, DefaultLanguage=$defLang" + } +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 4: Property values — enum properties --- +if ($propsNode) { + $enumChecked = 0 + $check4Ok = $true + + foreach ($propName in $validEnumValues.Keys) { + $propNode = $propsNode.SelectSingleNode("md:$propName", $ns) + if ($propNode -and $propNode.InnerText) { + $val = $propNode.InnerText + $allowed = $validEnumValues[$propName] + if ($allowed -notcontains $val) { + Report-Error "4. Property '$propName' has invalid value '$val'" + $check4Ok = $false + } + $enumChecked++ + } + } + + if ($check4Ok) { + Report-OK "4. Property values: $enumChecked enum properties checked" + } +} else { + Report-Warn "4. No Properties block to check" +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 5: ChildObjects — valid types, no duplicates, order --- +$childObjNode = $cfgNode.SelectSingleNode("md:ChildObjects", $ns) + +if (-not $childObjNode) { + Report-Error "5. ChildObjects block missing" +} else { + $check5Ok = $true + $totalCount = 0 + $typeCounts = @{} + $duplicates = @{} + $typeFirstIndex = @{} # type -> first position index + $lastTypeOrder = -1 + $orderOk = $true + $idx = 0 + + foreach ($child in $childObjNode.ChildNodes) { + if ($child.NodeType -ne 'Element') { continue } + $typeName = $child.LocalName + $objNameVal = $child.InnerText + + # Valid type? + $typeIdx = $childObjectTypes.IndexOf($typeName) + if ($typeIdx -lt 0) { + Report-Error "5. Unknown type '$typeName' in ChildObjects" + $check5Ok = $false + } else { + # Check order + if (-not $typeFirstIndex.ContainsKey($typeName)) { + $typeFirstIndex[$typeName] = $typeIdx + if ($typeIdx -lt $lastTypeOrder) { + Report-Warn "5. Type '$typeName' is out of canonical order (after type at position $lastTypeOrder)" + $orderOk = $false + } + $lastTypeOrder = $typeIdx + } + } + + # Count and dedup + if (-not $typeCounts.ContainsKey($typeName)) { $typeCounts[$typeName] = @{} } + if ($typeCounts[$typeName].ContainsKey($objNameVal)) { + if (-not $duplicates.ContainsKey("$typeName.$objNameVal")) { + Report-Error "5. Duplicate: $typeName.$objNameVal" + $duplicates["$typeName.$objNameVal"] = $true + $check5Ok = $false + } + } else { + $typeCounts[$typeName][$objNameVal] = $true + } + + $totalCount++ + $idx++ + } + + $typeCount = $typeCounts.Count + if ($check5Ok) { + $orderInfo = if ($orderOk) { ", order correct" } else { "" } + Report-OK "5. ChildObjects: $typeCount types, $totalCount objects${orderInfo}" + } +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 6: DefaultLanguage references existing Language in ChildObjects --- +if ($defLang -and $childObjNode) { + # DefaultLanguage is like "Language.Русский" + $langName = $defLang + if ($langName.StartsWith("Language.")) { + $langName = $langName.Substring(9) + } + + $found = $false + foreach ($child in $childObjNode.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Language" -and $child.InnerText -eq $langName) { + $found = $true; break + } + } + + if ($found) { + Report-OK "6. DefaultLanguage `"$defLang`" found in ChildObjects" + } else { + Report-Error "6. DefaultLanguage `"$defLang`" not found in ChildObjects" + } +} else { + if (-not $defLang) { + Report-Warn "6. Cannot check DefaultLanguage (empty)" + } else { + Report-Warn "6. Cannot check DefaultLanguage (no ChildObjects)" + } +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 7: Language files exist --- +if ($childObjNode) { + $langNames = @() + foreach ($child in $childObjNode.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Language") { + $langNames += $child.InnerText + } + } + + if ($langNames.Count -gt 0) { + $existCount = 0 + foreach ($ln in $langNames) { + $langFile = Join-Path (Join-Path $configDir "Languages") "$ln.xml" + if (Test-Path $langFile) { + $existCount++ + } else { + Report-Warn "7. Language file missing: Languages/$ln.xml" + } + } + if ($existCount -eq $langNames.Count) { + Report-OK "7. Language files: $existCount/$($langNames.Count) exist" + } + } else { + Report-Warn "7. No Language entries in ChildObjects" + } +} else { + Report-Warn "7. Cannot check language files (no ChildObjects)" +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 8: Object directories exist (spot-check) --- +if ($childObjNode) { + $dirsToCheck = @{} + foreach ($child in $childObjNode.ChildNodes) { + if ($child.NodeType -ne 'Element') { continue } + $typeName = $child.LocalName + if ($typeName -eq "Language") { continue } # Already checked + if ($childTypeDirMap.ContainsKey($typeName)) { + $dirName = $childTypeDirMap[$typeName] + if (-not $dirsToCheck.ContainsKey($dirName)) { + $dirsToCheck[$dirName] = 0 + } + $dirsToCheck[$dirName] = $dirsToCheck[$dirName] + 1 + } + } + + $missingDirs = @() + foreach ($dir in $dirsToCheck.Keys) { + $dirPath = Join-Path $configDir $dir + if (-not (Test-Path $dirPath -PathType Container)) { + $missingDirs += "$dir ($($dirsToCheck[$dir]) objects)" + } + } + + if ($missingDirs.Count -eq 0) { + Report-OK "8. Object directories: $($dirsToCheck.Count) directories, all exist" + } else { + foreach ($md in $missingDirs) { + Report-Warn "8. Missing directory: $md" + } + } +} + +# --- Check 9: Form references (HomePageWorkArea + Properties) --- +function Test-FormRef([string]$ref) { + if (-not $ref) { return $true } + # UUID — cannot verify without scanning all forms; skip + if ($ref -match $guidPattern) { return $true } + $parts = $ref.Split(".") + if ($parts.Count -eq 2 -and $parts[0] -eq "CommonForm") { + $p = Join-Path (Join-Path (Join-Path $configDir "CommonForms") $parts[1]) "Form.xml" + $pExt = Join-Path (Join-Path (Join-Path (Join-Path $configDir "CommonForms") $parts[1]) "Ext") "Form.xml" + return (Test-Path $p) -or (Test-Path $pExt) + } + if ($parts.Count -eq 4 -and $parts[2] -eq "Form" -and $childTypeDirMap.ContainsKey($parts[0])) { + $dir = $childTypeDirMap[$parts[0]] + $p = Join-Path (Join-Path (Join-Path (Join-Path (Join-Path $configDir $dir) $parts[1]) "Forms") $parts[3]) "Form.xml" + $pExt = Join-Path (Join-Path (Join-Path (Join-Path (Join-Path (Join-Path $configDir $dir) $parts[1]) "Forms") $parts[3]) "Ext") "Form.xml" + return (Test-Path $p) -or (Test-Path $pExt) + } + return $false +} + +$formRefsChecked = 0 +$formRefErrors = @() + +# HomePageWorkArea +$hpPath = Join-Path (Join-Path $configDir "Ext") "HomePageWorkArea.xml" +if (Test-Path $hpPath) { + try { + [xml]$hpDoc = Get-Content -Path $hpPath -Encoding UTF8 + $hpNs = New-Object System.Xml.XmlNamespaceManager($hpDoc.NameTable) + $hpNs.AddNamespace("hp", "http://v8.1c.ru/8.3/xcf/extrnprops") + foreach ($f in $hpDoc.DocumentElement.SelectNodes("//hp:Item/hp:Form", $hpNs)) { + $ref = $f.InnerText.Trim() + if (-not $ref) { continue } + $formRefsChecked++ + if (-not (Test-FormRef $ref)) { + $formRefErrors += "HomePageWorkArea.Form '$ref' — file not found" + } + } + } catch { + $formRefErrors += "HomePageWorkArea.xml: parse error — $($_.Exception.Message)" + } +} + +# Properties: DefaultXxxForm refs +if ($propsNode) { + $formProps = @("DefaultReportForm","DefaultReportVariantForm","DefaultReportSettingsForm","DefaultDynamicListSettingsForm","DefaultSearchForm","DefaultDataHistoryChangeHistoryForm","DefaultDataHistoryVersionDataForm","DefaultDataHistoryVersionDifferencesForm","DefaultCollaborationSystemUsersChoiceForm","DefaultConstantsForm") + foreach ($pn in $formProps) { + $node = $propsNode.SelectSingleNode("md:$pn", $ns) + if ($node -and $node.InnerText.Trim()) { + $ref = $node.InnerText.Trim() + $formRefsChecked++ + if (-not (Test-FormRef $ref)) { + $formRefErrors += "Properties.$pn '$ref' — form not found" + } + } + } +} + +if ($formRefsChecked -eq 0) { + Report-OK "9. Form references: none to check" +} elseif ($formRefErrors.Count -eq 0) { + Report-OK "9. Form references: $formRefsChecked verified" +} else { + foreach ($err in $formRefErrors) { Report-Error "9. $err" } +} + +# --- Final output --- +& $finalize + +if ($script:errors -gt 0) { + exit 1 +} +exit 0 diff --git a/.codex/skills/cf-validate/scripts/cf-validate.py b/.codex/skills/cf-validate/scripts/cf-validate.py new file mode 100644 index 00000000..df1426d9 --- /dev/null +++ b/.codex/skills/cf-validate/scripts/cf-validate.py @@ -0,0 +1,600 @@ +#!/usr/bin/env python3 +# cf-validate v1.3 — 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', 'Version8_5_1', + ], + 'DefaultRunMode': ['ManagedApplication', 'OrdinaryApplication', 'Auto'], + 'ScriptVariant': ['Russian', 'English'], + 'DataLockControlMode': ['Automatic', 'Managed', 'AutomaticAndManaged'], + 'ObjectAutonumerationMode': ['NotAutoFree', 'AutoFree'], + 'ModalityUseMode': ['DontUse', 'Use', 'UseWithWarnings'], + 'SynchronousPlatformExtensionAndAddInCallUseMode': ['DontUse', 'Use', 'UseWithWarnings'], + 'InterfaceCompatibilityMode': [ + 'Version8_2', 'Version8_2EnableTaxi', 'Taxi', 'TaxiEnableVersion8_2', + 'TaxiEnableVersion8_5', 'Version8_5EnableTaxi', 'Version8_5', + ], + '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', 'Version8_5_1', + ], +} + +EXPECTED_NS = 'http://v8.1c.ru/8.3/MDClasses' + + +class Reporter: + def __init__(self, max_errors, detailed=False): + self.errors = 0 + self.warnings = 0 + self.ok_count = 0 + self.stopped = False + self.max_errors = max_errors + self.detailed = detailed + self.lines = [] + self.obj_name = '(unknown)' + + def out(self, msg=''): + self.lines.append(msg) + + def ok(self, msg): + self.ok_count += 1 + if self.detailed: + 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): + checks = self.ok_count + self.errors + self.warnings + if self.errors == 0 and self.warnings == 0 and not self.detailed: + result = f'=== Validation OK: Configuration.{self.obj_name} ({checks} checks) ===' + else: + self.out('') + self.out(f'=== Result: {self.errors} errors, {self.warnings} warnings ({checks} checks) ===') + result = self.text() + + print(result, end='' if '\r\n' in result else '\n') + + 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(): + sys.stdout.reconfigure(encoding="utf-8") + sys.stderr.reconfigure(encoding="utf-8") + parser = argparse.ArgumentParser( + description='Validate 1C configuration XML structure', allow_abbrev=False + ) + parser.add_argument('-ConfigPath', '-Path', dest='ConfigPath', required=True) + parser.add_argument('-Detailed', action='store_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, detailed=args.Detailed) + 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', '2.21'): + r.warn(f"1. Unusual version '{version}' (expected 2.17, 2.20 or 2.21)") + + # 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.obj_name = obj_name + + 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: + pass # no ChildObjects + + # --- Check 9: Form references (HomePageWorkArea + Properties) --- + def test_form_ref(ref): + if not ref: + return True + if GUID_PATTERN.match(ref): + return True + parts = ref.split('.') + if len(parts) == 2 and parts[0] == 'CommonForm': + p = os.path.join(config_dir, 'CommonForms', parts[1], 'Form.xml') + p_ext = os.path.join(config_dir, 'CommonForms', parts[1], 'Ext', 'Form.xml') + return os.path.isfile(p) or os.path.isfile(p_ext) + if len(parts) == 4 and parts[2] == 'Form' and parts[0] in CHILD_TYPE_DIR_MAP: + d = CHILD_TYPE_DIR_MAP[parts[0]] + p = os.path.join(config_dir, d, parts[1], 'Forms', parts[3], 'Form.xml') + p_ext = os.path.join(config_dir, d, parts[1], 'Forms', parts[3], 'Ext', 'Form.xml') + return os.path.isfile(p) or os.path.isfile(p_ext) + return False + + form_refs_checked = 0 + form_ref_errors = [] + + hp_path = os.path.join(config_dir, 'Ext', 'HomePageWorkArea.xml') + if os.path.isfile(hp_path): + try: + hp_tree = etree.parse(hp_path) + HP_NS = 'http://v8.1c.ru/8.3/xcf/extrnprops' + for f in hp_tree.getroot().iter(f'{{{HP_NS}}}Form'): + ref = (f.text or '').strip() + if not ref: + continue + form_refs_checked += 1 + if not test_form_ref(ref): + form_ref_errors.append(f"HomePageWorkArea.Form '{ref}' — file not found") + except Exception as e: + form_ref_errors.append(f'HomePageWorkArea.xml: parse error — {e}') + + if props_node is not None: + form_props = ['DefaultReportForm','DefaultReportVariantForm','DefaultReportSettingsForm','DefaultDynamicListSettingsForm','DefaultSearchForm','DefaultDataHistoryChangeHistoryForm','DefaultDataHistoryVersionDataForm','DefaultDataHistoryVersionDifferencesForm','DefaultCollaborationSystemUsersChoiceForm','DefaultConstantsForm'] + for pn in form_props: + node = props_node.find(f'md:{pn}', NS) + if node is not None and node.text and node.text.strip(): + ref = node.text.strip() + form_refs_checked += 1 + if not test_form_ref(ref): + form_ref_errors.append(f"Properties.{pn} '{ref}' — form not found") + + if form_refs_checked == 0: + r.ok('9. Form references: none to check') + elif not form_ref_errors: + r.ok(f'9. Form references: {form_refs_checked} verified') + else: + for err in form_ref_errors: + r.error(f'9. {err}') + + # --- Final output --- + r.finalize(out_file) + sys.exit(1 if r.errors > 0 else 0) + + +if __name__ == '__main__': + main() diff --git a/.codex/skills/cfe-borrow/SKILL.md b/.codex/skills/cfe-borrow/SKILL.md new file mode 100644 index 00000000..bbaafcd3 --- /dev/null +++ b/.codex/skills/cfe-borrow/SKILL.md @@ -0,0 +1,101 @@ +--- +name: cfe-borrow +description: Заимствование объектов из конфигурации 1С в расширение (CFE). Используй когда нужно перехватить метод, изменить форму или добавить реквизит к существующему объекту конфигурации +argument-hint: -ExtensionPath -ConfigPath -Object "Catalog.Контрагенты.Form.ФормаЭлемента" -BorrowMainAttribute +allowed-tools: + - Bash + - Read + - Glob +--- + +# /cfe-borrow — Заимствование объектов из конфигурации + +Заимствует объекты из основной конфигурации в расширение. Создаёт XML-файлы с `ObjectBelonging=Adopted` и `ExtendedConfigurationObject`, добавляет запись в ChildObjects расширения. + +## Предусловие + +Расширение должно быть создано (`/cfe-init`) и содержать валидный `Configuration.xml`. + +### Авто-определение ConfigPath + +Если пользователь не указал `-ConfigPath` — попробуй определить автоматически: +1. Прочитай `.v8-project.json` из корня проекта +2. Разреши целевую базу (по имени, ветке или `default` — алгоритм из `/db-list`) +3. Если у базы есть поле `configSrc` — используй как `-ConfigPath` +4. Если `configSrc` нет — спроси у пользователя + +## Параметры + +| Параметр | Описание | +|----------|----------| +| `ExtensionPath` | Путь к каталогу расширения (обязат.) | +| `ConfigPath` | Путь к конфигурации-источнику (обязат.) | +| `Object` | Что заимствовать (обязат.), batch через `;;` | +| `BorrowMainAttribute` | Заимствовать основной реквизит формы. Без параметра — не заимствует. `Form` — реквизиты, используемые на форме. `All` — все реквизиты объекта. Требует форму в -Object | + +## Формат -Object + +- `Catalog.Контрагенты` — справочник +- `CommonModule.РаботаСФайлами` — общий модуль +- `Document.РеализацияТоваров` — документ +- `Enum.ВидыОплат` — перечисление +- `Catalog.Контрагенты.Form.ФормаЭлемента` — форма объекта (заимствование формы) +- `Catalog.X ;; CommonModule.Y ;; Enum.Z` — несколько объектов +Поддерживаются все 44 типа объектов конфигурации. + +### Заимствование форм + +Формат `Тип.Имя.Form.ИмяФормы` заимствует форму конкретного объекта. Если родительский объект ещё не заимствован — он будет заимствован автоматически. + +Создаётся: +1. **Метаданные формы** — `Forms/ИмяФормы.xml` с `ObjectBelonging=Adopted`, `FormType=Managed` +2. **Form.xml** — `Forms/ИмяФормы/Ext/Form.xml` с копией исходной формы + `` (начальное состояние) +3. **Module.bsl** — пустой файл `Forms/ИмяФормы/Ext/Form/Module.bsl` +4. **Регистрация** — `
` в ChildObjects родительского объекта + +### Заимствование основного реквизита формы (-BorrowMainAttribute) + +**Когда нужно**: пользователь хочет добавить новый реквизит в существующий объект конфигурации и вывести его на заимствованную форму. Без `-BorrowMainAttribute` форма заимствуется "пустой" — только визуальные элементы, без привязки к данным объекта. С `-BorrowMainAttribute` форма сохраняет привязки к реквизитам объекта (DataPath), что позволяет затем добавить на неё новые элементы через `/form-edit`. + +**Два режима**: +- `Form` (по умолчанию) — заимствует только те реквизиты объекта, которые уже выведены на форму. Оптимальный выбор для большинства случаев +- `All` — заимствует все реквизиты и табличные части объекта. Используй если планируешь выводить на форму реквизиты, которых на ней ещё нет + +**Типовой сценарий** (добавление реквизита + вывод на форму): +1. `/cfe-borrow` с `-BorrowMainAttribute` — заимствовать форму с реквизитами +2. `/meta-edit` — добавить новый реквизит в объект расширения +3. `/form-edit` — вывести реквизит на заимствованную форму + +**Защита существующих данных**: если зависимый объект уже заимствован с содержимым (реквизитами, формами) — скрипт не перезаписывает его, а добавляет только недостающее. + +## Команда + +```powershell +powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/cfe-borrow/scripts/cfe-borrow.ps1" -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты" +``` + +## Примеры + +```powershell +# Заимствовать один объект +... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты" + +# Заимствовать форму (автоматически заимствует родительский объект) +... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты.Form.ФормаЭлемента" + +# Несколько объектов за раз +... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты ;; CommonModule.ОбщийМодуль ;; Enum.ВидыОплат" + +# Заимствовать форму с основным реквизитом (реквизиты по DataPath формы) +... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Номенклатура.Form.ФормаЭлемента" -BorrowMainAttribute + +# Заимствовать форму с ВСЕМИ реквизитами объекта +... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Номенклатура.Form.ФормаЭлемента" -BorrowMainAttribute All +``` + +## Верификация + +``` +/cfe-validate +``` + diff --git a/.codex/skills/cfe-borrow/scripts/cfe-borrow.ps1 b/.codex/skills/cfe-borrow/scripts/cfe-borrow.ps1 new file mode 100644 index 00000000..099ddac2 --- /dev/null +++ b/.codex/skills/cfe-borrow/scripts/cfe-borrow.ps1 @@ -0,0 +1,1772 @@ +# cfe-borrow v1.3 — Borrow objects from configuration into extension (CFE) +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( + [Parameter(Mandatory)][string]$ExtensionPath, + [Parameter(Mandatory)][string]$ConfigPath, + [Parameter(Mandatory)][string]$Object, + [string]$BorrowMainAttribute +) + +$ErrorActionPreference = "Stop" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +function Info([string]$msg) { Write-Host "[INFO] $msg" } +function Warn([string]$msg) { Write-Host "[WARN] $msg" } + +# --- 1. Resolve paths --- +if (-not [System.IO.Path]::IsPathRooted($ExtensionPath)) { + $ExtensionPath = Join-Path (Get-Location).Path $ExtensionPath +} +if (Test-Path $ExtensionPath -PathType Container) { + $candidate = Join-Path $ExtensionPath "Configuration.xml" + if (Test-Path $candidate) { $ExtensionPath = $candidate } + else { Write-Error "No Configuration.xml in extension directory: $ExtensionPath"; exit 1 } +} +if (-not (Test-Path $ExtensionPath)) { Write-Error "Extension file not found: $ExtensionPath"; exit 1 } +$extResolvedPath = (Resolve-Path $ExtensionPath).Path +$extDir = Split-Path $extResolvedPath -Parent + +if (-not [System.IO.Path]::IsPathRooted($ConfigPath)) { + $ConfigPath = Join-Path (Get-Location).Path $ConfigPath +} +if (Test-Path $ConfigPath -PathType Container) { + $candidate = Join-Path $ConfigPath "Configuration.xml" + if (Test-Path $candidate) { $ConfigPath = $candidate } + else { Write-Error "No Configuration.xml in config directory: $ConfigPath"; exit 1 } +} +if (-not (Test-Path $ConfigPath)) { Write-Error "Config file not found: $ConfigPath"; exit 1 } +$cfgResolvedPath = (Resolve-Path $ConfigPath).Path +$cfgDir = Split-Path $cfgResolvedPath -Parent + +# --- 2. Load extension Configuration.xml --- +$script:xmlDoc = New-Object System.Xml.XmlDocument +$script:xmlDoc.PreserveWhitespace = $true +$script:xmlDoc.Load($extResolvedPath) + +$script:mdNs = "http://v8.1c.ru/8.3/MDClasses" +$script:xrNs = "http://v8.1c.ru/8.3/xcf/readable" +$script:xsiNs = "http://www.w3.org/2001/XMLSchema-instance" +$script:v8Ns = "http://v8.1c.ru/8.1/data/core" + +$root = $script:xmlDoc.DocumentElement + +$script:cfgEl = $null +foreach ($child in $root.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Configuration") { + $script:cfgEl = $child; break + } +} +if (-not $script:cfgEl) { Write-Error "No element found in extension"; exit 1 } + +$script:propsEl = $null +$script:childObjsEl = $null +foreach ($child in $script:cfgEl.ChildNodes) { + if ($child.NodeType -ne 'Element') { continue } + if ($child.LocalName -eq "Properties") { $script:propsEl = $child } + if ($child.LocalName -eq "ChildObjects") { $script:childObjsEl = $child } +} + +if (-not $script:propsEl) { Write-Error "No element found in extension"; exit 1 } +if (-not $script:childObjsEl) { Write-Error "No element found in extension"; exit 1 } + +# --- 3. Extract NamePrefix --- +$script:namePrefix = "" +foreach ($child in $script:propsEl.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "NamePrefix") { + $script:namePrefix = $child.InnerText.Trim(); break + } +} +Info "Extension NamePrefix: $($script:namePrefix)" + +# --- 4. Type mappings --- +$childTypeDirMap = @{ + "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" +} + +# --- 4b. Russian synonym → English type --- +$synonymMap = @{ + "Справочник"="Catalog"; "Документ"="Document"; "Перечисление"="Enum" + "ОбщийМодуль"="CommonModule"; "ОбщаяКартинка"="CommonPicture" + "ОбщаяКоманда"="CommonCommand"; "ОбщийМакет"="CommonTemplate" + "ПланОбмена"="ExchangePlan"; "Отчет"="Report"; "Отчёт"="Report" + "Обработка"="DataProcessor"; "РегистрСведений"="InformationRegister" + "РегистрНакопления"="AccumulationRegister" + "ПланВидовХарактеристик"="ChartOfCharacteristicTypes" + "ПланСчетов"="ChartOfAccounts"; "РегистрБухгалтерии"="AccountingRegister" + "ПланВидовРасчета"="ChartOfCalculationTypes"; "РегистрРасчета"="CalculationRegister" + "БизнесПроцесс"="BusinessProcess"; "Задача"="Task" + "Подсистема"="Subsystem"; "Роль"="Role"; "Константа"="Constant" + "ФункциональнаяОпция"="FunctionalOption"; "ОпределяемыйТип"="DefinedType" + "ОбщаяФорма"="CommonForm"; "ЖурналДокументов"="DocumentJournal" + "ПараметрСеанса"="SessionParameter"; "ГруппаКоманд"="CommandGroup" + "ПодпискаНаСобытие"="EventSubscription"; "РегламентноеЗадание"="ScheduledJob" + "ОбщийРеквизит"="CommonAttribute"; "ПакетXDTO"="XDTOPackage" + "HTTPСервис"="HTTPService"; "СервисИнтеграции"="IntegrationService" +} + +# --- 5. Canonical type order (44 types) --- +$script:typeOrder = @( + "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" +) + +# --- 6. GeneratedType patterns per type --- +$script:generatedTypes = @{ + "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" } + ) + "DefinedType" = @( + @{ prefix = "DefinedType"; category = "DefinedType" } + ) +} + +# Types that need ChildObjects element +$typesWithChildObjects = @( + "Catalog","Document","ExchangePlan","ChartOfAccounts", + "ChartOfCharacteristicTypes","ChartOfCalculationTypes", + "BusinessProcess","Task","Enum", + "InformationRegister","AccumulationRegister","AccountingRegister","CalculationRegister" +) + +# CommonModule properties to copy from source +$commonModuleProps = @("Global","ClientManagedApplication","Server","ExternalConnection","ClientOrdinaryApplication","ServerCall") + +# Standard system fields to skip when collecting DataPath references +$script:standardFields = @("Code","Description","Ref","Parent","DeletionMark","Predefined","IsFolder","LineNumber","RowsCount","PredefinedDataName") + +# --- 7. XML manipulation helpers (from cf-edit) --- +function Get-ChildIndent($container) { + foreach ($child in $container.ChildNodes) { + if ($child.NodeType -eq 'Whitespace' -or $child.NodeType -eq 'SignificantWhitespace') { + if ($child.Value -match '^\r?\n(\t+)$') { return $Matches[1] } + if ($child.Value -match '^\r?\n(\t+)') { return $Matches[1] } + } + } + $depth = 0; $current = $container + while ($current -and $current -ne $script:xmlDoc.DocumentElement) { $depth++; $current = $current.ParentNode } + return "`t" * ($depth + 1) +} + +function Insert-BeforeElement($container, $newNode, $refNode, $childIndent) { + $ws = $script:xmlDoc.CreateWhitespace("`r`n$childIndent") + if ($refNode) { + $container.InsertBefore($ws, $refNode) | Out-Null + $container.InsertBefore($newNode, $ws) | Out-Null + } else { + $trailing = $container.LastChild + if ($trailing -and ($trailing.NodeType -eq 'Whitespace' -or $trailing.NodeType -eq 'SignificantWhitespace')) { + $container.InsertBefore($ws, $trailing) | Out-Null + $container.InsertBefore($newNode, $trailing) | Out-Null + } else { + $container.AppendChild($ws) | Out-Null + $container.AppendChild($newNode) | Out-Null + $parentIndent = if ($childIndent.Length -gt 1) { $childIndent.Substring(0, $childIndent.Length - 1) } else { "" } + $closeWs = $script:xmlDoc.CreateWhitespace("`r`n$parentIndent") + $container.AppendChild($closeWs) | Out-Null + } + } +} + +function Expand-SelfClosingElement($container, $parentIndent) { + if (-not $container.HasChildNodes -or $container.IsEmpty) { + $closeWs = $script:xmlDoc.CreateWhitespace("`r`n$parentIndent") + $container.AppendChild($closeWs) | Out-Null + } +} + +# --- 7b. Detect format version --- + +function Detect-FormatVersion([string]$dir) { + $d = $dir + while ($d) { + $cfgPath = Join-Path $d "Configuration.xml" + if (Test-Path $cfgPath) { + $head = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8).Substring(0, [Math]::Min(2000, (Get-Item $cfgPath).Length)) + if ($head -match ']+version="(\d+\.\d+)"') { return $Matches[1] } + } + $parent = Split-Path $d -Parent + if ($parent -eq $d) { break } + $d = $parent + } + return "2.17" +} + +$script:formatVersion = Detect-FormatVersion $extDir + +# --- 8. Namespaces declaration for object XML --- +$script:xmlnsDecl = '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"' + +# --- 9. Parse -Object into items --- +$items = @() +foreach ($part in $Object.Split(";;")) { + $trimmed = $part.Trim() + if ($trimmed) { $items += $trimmed } +} + +if ($items.Count -eq 0) { + Write-Error "No objects specified in -Object" + exit 1 +} + +# --- 9b. Validate -BorrowMainAttribute --- +if ($BorrowMainAttribute) { + # PS treats -BorrowMainAttribute without value as "True" + if ($BorrowMainAttribute -eq "True") { $BorrowMainAttribute = "Form" } + if ($BorrowMainAttribute -notin @("Form","All")) { + Write-Error "-BorrowMainAttribute accepts 'Form' or 'All' (default: Form)" + exit 1 + } + # Validate: only with .Form. pattern + $hasForm = $false + foreach ($item in $items) { if ($item -match '\.Form\.') { $hasForm = $true; break } } + if (-not $hasForm) { + Write-Error "-BorrowMainAttribute requires a form in -Object (e.g. 'Catalog.X.Form.Y')" + exit 1 + } +} + +# --- 10. Helper: read source object XML --- +function Read-SourceObject { + param([string]$typeName, [string]$objName) + + $dirName = $childTypeDirMap[$typeName] + if (-not $dirName) { + Write-Error "Unknown type '$typeName'" + exit 1 + } + + $srcFile = Join-Path (Join-Path $cfgDir $dirName) "${objName}.xml" + if (-not (Test-Path $srcFile)) { + Write-Error "Source object not found: $srcFile" + exit 1 + } + + $srcDoc = New-Object System.Xml.XmlDocument + $srcDoc.PreserveWhitespace = $false + $srcDoc.Load($srcFile) + + $srcNs = New-Object System.Xml.XmlNamespaceManager($srcDoc.NameTable) + $srcNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") + $srcNs.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable") + + # Find the type element (e.g. ) + $srcRoot = $srcDoc.DocumentElement + $srcEl = $null + foreach ($c in $srcRoot.ChildNodes) { + if ($c.NodeType -eq 'Element') { $srcEl = $c; break } + } + if (-not $srcEl) { + Write-Error "No metadata element found in ${dirName}/${objName}.xml" + exit 1 + } + + # Extract uuid + $srcUuid = $srcEl.GetAttribute("uuid") + if (-not $srcUuid) { + Write-Error "No uuid attribute on source element in ${dirName}/${objName}.xml" + exit 1 + } + + # Extract properties for CommonModule + $srcProps = @{} + $propsNode = $srcEl.SelectSingleNode("md:Properties", $srcNs) + if ($propsNode) { + foreach ($propName in $commonModuleProps) { + $propNode = $propsNode.SelectSingleNode("md:${propName}", $srcNs) + if ($propNode) { + $srcProps[$propName] = $propNode.InnerText.Trim() + } + } + } + + return @{ + Uuid = $srcUuid + Properties = $srcProps + Element = $srcEl + NsManager = $srcNs + } +} + +# --- 10b. Helper: read source form UUID --- +function Read-SourceFormUuid { + param([string]$typeName, [string]$objName, [string]$formName) + + $dirName = $childTypeDirMap[$typeName] + $srcFile = Join-Path (Join-Path (Join-Path (Join-Path $cfgDir $dirName) $objName) "Forms") "${formName}.xml" + if (-not (Test-Path $srcFile)) { + Write-Error "Source form not found: $srcFile" + exit 1 + } + + $srcDoc = New-Object System.Xml.XmlDocument + $srcDoc.PreserveWhitespace = $false + $srcDoc.Load($srcFile) + + $srcEl = $null + foreach ($c in $srcDoc.DocumentElement.ChildNodes) { + if ($c.NodeType -eq 'Element') { $srcEl = $c; break } + } + if (-not $srcEl) { + Write-Error "No metadata element found in source form: $srcFile" + exit 1 + } + + $srcUuid = $srcEl.GetAttribute("uuid") + if (-not $srcUuid) { + Write-Error "No uuid attribute on source form element: $srcFile" + exit 1 + } + + return $srcUuid +} + +# --- 10c. Helper: borrow a form --- +function Borrow-Form { + param([string]$typeName, [string]$objName, [string]$formName, [switch]$BorrowMainAttr) + + $dirName = $childTypeDirMap[$typeName] + $enc = New-Object System.Text.UTF8Encoding($true) + + # 1. Read source form UUID + $formUuid = Read-SourceFormUuid $typeName $objName $formName + Info " Source form UUID: $formUuid" + + # 2. Read source Form.xml content + $srcFormXmlPath = Join-Path (Join-Path (Join-Path (Join-Path (Join-Path $cfgDir $dirName) $objName) "Forms") $formName) "Ext/Form.xml" + if (-not (Test-Path $srcFormXmlPath)) { + Write-Error "Source Form.xml not found: $srcFormXmlPath" + exit 1 + } + $srcFormContent = [System.IO.File]::ReadAllText($srcFormXmlPath, $enc) + + # 3. Generate form metadata XML (ФормаЭлемента.xml) + $newFormUuid = [guid]::NewGuid().ToString() + $formMetaSb = New-Object System.Text.StringBuilder + $formMetaSb.AppendLine("") | Out-Null + $formMetaSb.AppendLine("") | Out-Null + $formMetaSb.AppendLine("`t") | Out-Null + $formMetaSb.AppendLine("`t`t") | Out-Null + $formMetaSb.AppendLine("`t`t") | Out-Null + $formMetaSb.AppendLine("`t`t`tAdopted") | Out-Null + $formMetaSb.AppendLine("`t`t`t${formName}") | Out-Null + $formMetaSb.AppendLine("`t`t`t") | Out-Null + $formMetaSb.AppendLine("`t`t`t${formUuid}") | Out-Null + $formMetaSb.AppendLine("`t`t`tManaged") | Out-Null + $formMetaSb.AppendLine("`t`t") | Out-Null + $formMetaSb.AppendLine("`t") | Out-Null + $formMetaSb.Append("") | Out-Null + + # 4. Create directories + $formMetaDir = Join-Path (Join-Path (Join-Path $extDir $dirName) $objName) "Forms" + if (-not (Test-Path $formMetaDir)) { + New-Item -ItemType Directory -Path $formMetaDir -Force | Out-Null + } + + # Write form metadata + $formMetaFile = Join-Path $formMetaDir "${formName}.xml" + [System.IO.File]::WriteAllText($formMetaFile, $formMetaSb.ToString(), $enc) + Info " Created: $formMetaFile" + + # 5. Generate Form.xml with BaseForm (visual elements only) + # Parse source Form.xml as XmlDocument + $srcFormDoc = New-Object System.Xml.XmlDocument + $srcFormDoc.PreserveWhitespace = $true + $srcFormDoc.Load($srcFormXmlPath) + $srcFormEl = $srcFormDoc.DocumentElement + + $formVersion = $srcFormEl.GetAttribute("version") + if (-not $formVersion) { $formVersion = $script:formatVersion } + + # Find direct children: form properties, AutoCommandBar, ChildItems + $srcAutoCmd = $null + $srcChildItems = $null + $formProps = @() + $reachedVisual = $false + foreach ($fc in $srcFormEl.ChildNodes) { + if ($fc.NodeType -ne 'Element') { continue } + if ($fc.LocalName -eq 'AutoCommandBar' -and -not $srcAutoCmd) { + $reachedVisual = $true; $srcAutoCmd = $fc; continue + } + if ($fc.LocalName -eq 'ChildItems' -and -not $srcChildItems) { + $reachedVisual = $true; $srcChildItems = $fc; continue + } + if ($fc.LocalName -eq 'Events' -or $fc.LocalName -eq 'Attributes' -or $fc.LocalName -eq 'Commands' -or $fc.LocalName -eq 'Parameters' -or $fc.LocalName -eq 'CommandSet') { + $reachedVisual = $true; continue + } + if (-not $reachedVisual) { + $formProps += $fc.OuterXml + } + } + + # Get OuterXml and strip redundant namespace redeclarations (they're on root
) + $nsStripPattern = '\s+xmlns(?::\w+)?="[^"]*"' + + # AutoCommandBar: keep ChildItems (buttons with CommandName→0), Autofill→false + $autoCmdXml = "" + if ($srcAutoCmd) { + $autoCmdXml = $srcAutoCmd.OuterXml + $autoCmdXml = [regex]::Replace($autoCmdXml, $nsStripPattern, '') + $autoCmdXml = [regex]::Replace($autoCmdXml, '[^<]*', '0') + $autoCmdXml = $autoCmdXml -replace 'true', 'false' + # Strip ExcludedCommand (references to standard commands invalid in extension) + $autoCmdXml = [regex]::Replace($autoCmdXml, '\s*[^<]*', '') + # Strip DataPath in AutoCommandBar buttons + if ($BorrowMainAttr) { + # Keep only Объект.* DataPaths + $autoCmdXml = [regex]::Replace($autoCmdXml, '\s*(?!Объект\.)[^<]*', '') + } else { + $autoCmdXml = [regex]::Replace($autoCmdXml, '\s*[^<]*', '') + } + } + + # ChildItems: copy full tree, clean up base-config references + $childItemsXml = "" + if ($srcChildItems) { + $childItemsXml = $srcChildItems.OuterXml + $childItemsXml = [regex]::Replace($childItemsXml, $nsStripPattern, '') + # Replace all CommandName values with 0 + $childItemsXml = [regex]::Replace($childItemsXml, '[^<]*', '0') + # Strip DataPath, TitleDataPath, RowPictureDataPath + if ($BorrowMainAttr) { + # Keep only Объект.* DataPaths — strip form-attribute DataPaths (not borrowed) + $childItemsXml = [regex]::Replace($childItemsXml, '\s*(?!Объект\.)[^<]*', '') + $childItemsXml = [regex]::Replace($childItemsXml, '\s*(?!Объект\.)[^<]*', '') + $childItemsXml = [regex]::Replace($childItemsXml, '\s*[^<]*', '') + } else { + $childItemsXml = [regex]::Replace($childItemsXml, '\s*[^<]*', '') + $childItemsXml = [regex]::Replace($childItemsXml, '\s*[^<]*', '') + $childItemsXml = [regex]::Replace($childItemsXml, '\s*[^<]*', '') + } + # Strip ExcludedCommand in nested AutoCommandBars (references to standard commands invalid in extension) + $childItemsXml = [regex]::Replace($childItemsXml, '\s*[^<]*', '') + # Strip TypeLink blocks with human-readable DataPath (Items.XXX — can't convert to UUID) + $childItemsXml = [regex]::Replace($childItemsXml, '(?s)\s*\s*Items\.[^<]*.*?', '') + # Strip element-level Events (base form handlers not in extension) + $childItemsXml = [regex]::Replace($childItemsXml, '(?s)\s*.*?', '') + + # Collect CommonPicture references from ChildItems and AutoCommandBar + $referencedPictures = @{} + $picRefs = [regex]::Matches($childItemsXml, 'CommonPicture\.(\w+)') + foreach ($m in $picRefs) { $referencedPictures[$m.Groups[1].Value] = $true } + if ($autoCmdXml) { + $picRefs2 = [regex]::Matches($autoCmdXml, 'CommonPicture\.(\w+)') + foreach ($m in $picRefs2) { $referencedPictures[$m.Groups[1].Value] = $true } + } + + # Auto-borrow referenced CommonPictures (if not already borrowed) + $autoBorrowedPics = @() + foreach ($picName in $referencedPictures.Keys) { + if (-not (Test-ObjectBorrowed "CommonPicture" $picName)) { + $picSrcFile = Join-Path (Join-Path $cfgDir "CommonPictures") "${picName}.xml" + if (Test-Path $picSrcFile) { + $src = Read-SourceObject "CommonPicture" $picName + $borrowedXml = Build-BorrowedObjectXml "CommonPicture" $picName $src.Uuid $src.Properties + $targetDir = Join-Path $extDir "CommonPictures" + if (-not (Test-Path $targetDir)) { + New-Item -ItemType Directory -Path $targetDir -Force | Out-Null + } + $targetFile = Join-Path $targetDir "${picName}.xml" + $encBom = New-Object System.Text.UTF8Encoding($true) + [System.IO.File]::WriteAllText($targetFile, $borrowedXml, $encBom) + Add-ToChildObjects "CommonPicture" $picName + $autoBorrowedPics += $picName + $script:borrowedFiles += $targetFile + Info " Auto-borrowed: CommonPicture.${picName}" + } else { + Warn " CommonPicture.${picName} not found in source config — will strip from form" + } + } + } + + # Collect all borrowed CommonPictures (including previously borrowed) + $borrowedPicSet = @{} + $nsMgr2 = New-Object System.Xml.XmlNamespaceManager($script:xmlDoc.NameTable) + $nsMgr2.AddNamespace("md", $script:mdNs) + $picNodes = $script:xmlDoc.SelectNodes("//md:ChildObjects/md:CommonPicture", $nsMgr2) + foreach ($pn in $picNodes) { $borrowedPicSet[$pn.InnerText] = $true } + + # Strip blocks referencing non-borrowed CommonPictures + $picBlockPattern = '(?s)\s*\s*CommonPicture\.(\w+).*?' + $picMatches = [regex]::Matches($childItemsXml, $picBlockPattern) + # Process in reverse order to preserve positions + for ($mi = $picMatches.Count - 1; $mi -ge 0; $mi--) { + $pm = $picMatches[$mi] + $cpName = $pm.Groups[1].Value + if (-not $borrowedPicSet.ContainsKey($cpName)) { + $childItemsXml = $childItemsXml.Remove($pm.Index, $pm.Length) + } + } + # Strip StdPicture blocks (except Print) + $childItemsXml = [regex]::Replace($childItemsXml, '(?s)\s*\s*StdPicture\.(?!Print\b)\w+.*?', '') + + # Same Picture strip for AutoCommandBar + if ($autoCmdXml) { + $acPicMatches = [regex]::Matches($autoCmdXml, $picBlockPattern) + for ($mi = $acPicMatches.Count - 1; $mi -ge 0; $mi--) { + $pm = $acPicMatches[$mi] + $cpName = $pm.Groups[1].Value + if (-not $borrowedPicSet.ContainsKey($cpName)) { + $autoCmdXml = $autoCmdXml.Remove($pm.Index, $pm.Length) + } + } + $autoCmdXml = [regex]::Replace($autoCmdXml, '(?s)\s*\s*StdPicture\.(?!Print\b)\w+.*?', '') + } + + # Auto-borrow StyleItems referenced in ChildItems + # Pattern 1: , + # Pattern 2: style:XXX, style:XXX, etc. + $referencedStyles = @{} + $styleRefs1 = [regex]::Matches($childItemsXml, 'ref="style:(\w+)"[^>]*kind="StyleItem"') + foreach ($m in $styleRefs1) { $referencedStyles[$m.Groups[1].Value] = $true } + $styleRefs2 = [regex]::Matches($childItemsXml, '>style:(\w+)') + foreach ($m in $styleRefs2) { $referencedStyles[$m.Groups[1].Value] = $true } + + foreach ($styleName in $referencedStyles.Keys) { + if (-not (Test-ObjectBorrowed "StyleItem" $styleName)) { + $styleSrcFile = Join-Path (Join-Path $cfgDir "StyleItems") "${styleName}.xml" + if (Test-Path $styleSrcFile) { + $src = Read-SourceObject "StyleItem" $styleName + $borrowedXml = Build-BorrowedObjectXml "StyleItem" $styleName $src.Uuid $src.Properties + $targetDir = Join-Path $extDir "StyleItems" + if (-not (Test-Path $targetDir)) { + New-Item -ItemType Directory -Path $targetDir -Force | Out-Null + } + $targetFile = Join-Path $targetDir "${styleName}.xml" + $encBom = New-Object System.Text.UTF8Encoding($true) + [System.IO.File]::WriteAllText($targetFile, $borrowedXml, $encBom) + Add-ToChildObjects "StyleItem" $styleName + $script:borrowedFiles += $targetFile + Info " Auto-borrowed: StyleItem.${styleName}" + } else { + Warn " StyleItem.${styleName} not found in source config" + } + } + } + # Auto-borrow Enums + EnumValues referenced via DesignTimeRef in ChoiceParameters + # Collect Enum -> [EnumValue names] map + $dtRefs = [regex]::Matches($childItemsXml, 'xr:DesignTimeRef">Enum\.(\w+)\.EnumValue\.(\w+)') + $referencedEnumValues = @{} + foreach ($m in $dtRefs) { + $eName = $m.Groups[1].Value + $evName = $m.Groups[2].Value + if (-not $referencedEnumValues.ContainsKey($eName)) { $referencedEnumValues[$eName] = @{} } + $referencedEnumValues[$eName][$evName] = $true + } + + foreach ($enumName in $referencedEnumValues.Keys) { + if (-not (Test-ObjectBorrowed "Enum" $enumName)) { + $enumSrcFile = Join-Path (Join-Path $cfgDir "Enums") "${enumName}.xml" + if (Test-Path $enumSrcFile) { + # Read source Enum to get UUID and EnumValue UUIDs + $srcParser = New-Object System.Xml.XmlDocument + $srcParser.PreserveWhitespace = $true + $srcParser.Load($enumSrcFile) + $srcEnumEl = $null + foreach ($cn in $srcParser.DocumentElement.ChildNodes) { + if ($cn.NodeType -eq 'Element') { $srcEnumEl = $cn; break } + } + $srcEnumUuid = $srcEnumEl.GetAttribute("uuid") + + # Find source EnumValues by name + $enumValueXmls = @() + $neededValues = $referencedEnumValues[$enumName] + $srcNsMgr = New-Object System.Xml.XmlNamespaceManager($srcParser.NameTable) + $srcNsMgr.AddNamespace("md", $script:mdNs) + $srcEvNodes = $srcEnumEl.SelectNodes("md:ChildObjects/md:EnumValue", $srcNsMgr) + foreach ($evNode in $srcEvNodes) { + $evUuid = $evNode.GetAttribute("uuid") + $evNameNode = $evNode.SelectSingleNode("md:Properties/md:Name", $srcNsMgr) + if ($evNameNode -and $neededValues.ContainsKey($evNameNode.InnerText)) { + $newEvUuid = [guid]::NewGuid().ToString() + $enumValueXmls += @" + + + + Adopted + $($evNameNode.InnerText) + + ${evUuid} + + +"@ + } + } + + # Build borrowed Enum with EnumValues in ChildObjects + $src = Read-SourceObject "Enum" $enumName + $borrowedXml = Build-BorrowedObjectXml "Enum" $enumName $src.Uuid $src.Properties + if ($enumValueXmls.Count -gt 0) { + $evBlock = ($enumValueXmls -join "`r`n") + $borrowedXml = $borrowedXml -replace '', "`r`n${evBlock}`r`n`t`t" + } + + $targetDir = Join-Path $extDir "Enums" + if (-not (Test-Path $targetDir)) { + New-Item -ItemType Directory -Path $targetDir -Force | Out-Null + } + $targetFile = Join-Path $targetDir "${enumName}.xml" + $encBom = New-Object System.Text.UTF8Encoding($true) + [System.IO.File]::WriteAllText($targetFile, $borrowedXml, $encBom) + Add-ToChildObjects "Enum" $enumName + $script:borrowedFiles += $targetFile + Info " Auto-borrowed: Enum.${enumName} (with $($enumValueXmls.Count) EnumValue(s))" + } else { + Warn " Enum.${enumName} not found in source config" + } + } + } + } + + # Extract the opening tag from source text (preserves namespace declarations) + $xmlDecl = '' + $formTag = "" + if ($srcFormContent -match '(?s)^(<\?xml[^?]*\?>)') { $xmlDecl = $Matches[1] } + if ($srcFormContent -match '(]*>)') { $formTag = $Matches[1] } + + # Build output Form.xml + $formXmlSb = New-Object System.Text.StringBuilder + $formXmlSb.Append($xmlDecl) | Out-Null + $formXmlSb.Append("`r`n") | Out-Null + $formXmlSb.Append($formTag) | Out-Null + $formXmlSb.Append("`r`n") | Out-Null + + # Part 1: form properties + AutoCommandBar + ChildItems + foreach ($propXml in $formProps) { + $propXml = [regex]::Replace($propXml, $nsStripPattern, '') + $formXmlSb.Append("`t$propXml`r`n") | Out-Null + } + if ($autoCmdXml) { + $formXmlSb.Append("`t$autoCmdXml") | Out-Null + $formXmlSb.Append("`r`n") | Out-Null + } + if ($childItemsXml) { + $formXmlSb.Append("`t$childItemsXml") | Out-Null + $formXmlSb.Append("`r`n") | Out-Null + } + # Attributes: empty or with MainAttribute when BorrowMainAttr + if ($BorrowMainAttr) { + $objTypePrefix = "" + $gtList = $script:generatedTypes[$typeName] + if ($gtList) { foreach ($g in $gtList) { if ($g.category -eq "Object") { $objTypePrefix = $g.prefix; break } } } + $mainAttrType = "cfg:${objTypePrefix}.${objName}" + $formXmlSb.Append("`t`r`n") | Out-Null + $formXmlSb.Append("`t`t`r`n") | Out-Null + $formXmlSb.Append("`t`t`t${mainAttrType}`r`n") | Out-Null + $formXmlSb.Append("`t`t`ttrue`r`n") | Out-Null + $formXmlSb.Append("`t`t`ttrue`r`n") | Out-Null + $formXmlSb.Append("`t`t`r`n") | Out-Null + $formXmlSb.Append("`t") | Out-Null + } else { + $formXmlSb.Append("`t") | Out-Null + } + $formXmlSb.Append("`r`n") | Out-Null + + # BaseForm: same content, indented one more level + $formXmlSb.Append("`t") | Out-Null + $formXmlSb.Append("`r`n") | Out-Null + + foreach ($propXml in $formProps) { + $propXml = [regex]::Replace($propXml, $nsStripPattern, '') + $formXmlSb.Append("`t`t$propXml`r`n") | Out-Null + } + if ($autoCmdXml) { + $acLines = $autoCmdXml -split "`r?`n" + for ($li = 0; $li -lt $acLines.Count; $li++) { + if ($li -eq 0) { $formXmlSb.Append("`t`t$($acLines[$li])") | Out-Null } + else { $formXmlSb.Append("`t$($acLines[$li])") | Out-Null } + $formXmlSb.Append("`r`n") | Out-Null + } + } + if ($childItemsXml) { + # Reindent ChildItems for BaseForm (+1 tab level) + $ciLines = $childItemsXml -split "`r?`n" + for ($li = 0; $li -lt $ciLines.Count; $li++) { + if ($li -eq 0) { $formXmlSb.Append("`t`t$($ciLines[$li])") | Out-Null } + else { $formXmlSb.Append("`t$($ciLines[$li])") | Out-Null } + $formXmlSb.Append("`r`n") | Out-Null + } + } + + # BaseForm Attributes: same as main section + if ($BorrowMainAttr) { + $formXmlSb.Append("`t`t`r`n") | Out-Null + $formXmlSb.Append("`t`t`t`r`n") | Out-Null + $formXmlSb.Append("`t`t`t`t${mainAttrType}`r`n") | Out-Null + $formXmlSb.Append("`t`t`t`ttrue`r`n") | Out-Null + $formXmlSb.Append("`t`t`t`ttrue`r`n") | Out-Null + $formXmlSb.Append("`t`t`t`r`n") | Out-Null + $formXmlSb.Append("`t`t") | Out-Null + } else { + $formXmlSb.Append("`t`t") | Out-Null + } + $formXmlSb.Append("`r`n") | Out-Null + $formXmlSb.Append("`t") | Out-Null + $formXmlSb.Append("`r`n") | Out-Null + $formXmlSb.Append("") | Out-Null + + # Write Form.xml + $formXmlDir = Join-Path (Join-Path $formMetaDir $formName) "Ext" + if (-not (Test-Path $formXmlDir)) { + New-Item -ItemType Directory -Path $formXmlDir -Force | Out-Null + } + $formXmlFile = Join-Path $formXmlDir "Form.xml" + [System.IO.File]::WriteAllText($formXmlFile, $formXmlSb.ToString(), $enc) + Info " Created: $formXmlFile" + + # 6. Create empty Module.bsl + $moduleDir = Join-Path $formXmlDir "Form" + if (-not (Test-Path $moduleDir)) { + New-Item -ItemType Directory -Path $moduleDir -Force | Out-Null + } + $moduleBslFile = Join-Path $moduleDir "Module.bsl" + [System.IO.File]::WriteAllText($moduleBslFile, "", $enc) + Info " Created: $moduleBslFile" + + # 7. Register form in parent object ChildObjects + Register-FormInObject $typeName $objName $formName + + return @($formMetaFile, $formXmlFile, $moduleBslFile) +} + +# --- 10d. Helper: register form in parent object's ChildObjects --- +function Register-FormInObject { + param([string]$typeName, [string]$objName, [string]$formName) + + $dirName = $childTypeDirMap[$typeName] + $objFile = Join-Path (Join-Path $extDir $dirName) "${objName}.xml" + + if (-not (Test-Path $objFile)) { + Warn "Parent object file not found: $objFile — form not registered in ChildObjects" + return + } + + $objDoc = New-Object System.Xml.XmlDocument + $objDoc.PreserveWhitespace = $true + $objDoc.Load($objFile) + + $objNs = New-Object System.Xml.XmlNamespaceManager($objDoc.NameTable) + $objNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") + + # Find the type element + $objEl = $null + foreach ($c in $objDoc.DocumentElement.ChildNodes) { + if ($c.NodeType -eq 'Element') { $objEl = $c; break } + } + if (-not $objEl) { + Warn "No type element in $objFile — form not registered" + return + } + + # Find or create ChildObjects + $childObjs = $objEl.SelectSingleNode("md:ChildObjects", $objNs) + if (-not $childObjs) { + # Create ChildObjects element + $childObjs = $objDoc.CreateElement("ChildObjects", "http://v8.1c.ru/8.3/MDClasses") + $objEl.AppendChild($objDoc.CreateWhitespace("`r`n`t`t")) | Out-Null + $objEl.AppendChild($childObjs) | Out-Null + $objEl.AppendChild($objDoc.CreateWhitespace("`r`n`t")) | Out-Null + } + + # Check dedup + foreach ($c in $childObjs.ChildNodes) { + if ($c.NodeType -eq 'Element' -and $c.LocalName -eq "Form" -and $c.InnerText -eq $formName) { + Warn "Form '$formName' already in ChildObjects of ${typeName}.${objName}" + return + } + } + + # Expand self-closing if needed + if (-not $childObjs.HasChildNodes -or $childObjs.IsEmpty) { + $closeWs = $objDoc.CreateWhitespace("`r`n`t`t") + $childObjs.AppendChild($closeWs) | Out-Null + } + + # Add
formName
+ $formEl = $objDoc.CreateElement("Form", "http://v8.1c.ru/8.3/MDClasses") + $formEl.InnerText = $formName + + $trailing = $childObjs.LastChild + $ws = $objDoc.CreateWhitespace("`r`n`t`t`t") + if ($trailing -and ($trailing.NodeType -eq 'Whitespace' -or $trailing.NodeType -eq 'SignificantWhitespace')) { + $childObjs.InsertBefore($ws, $trailing) | Out-Null + $childObjs.InsertBefore($formEl, $trailing) | Out-Null + } else { + $childObjs.AppendChild($ws) | Out-Null + $childObjs.AppendChild($formEl) | Out-Null + } + + # Save object XML + $settings2 = New-Object System.Xml.XmlWriterSettings + $settings2.Encoding = New-Object System.Text.UTF8Encoding($true) + $settings2.Indent = $false + $settings2.NewLineHandling = [System.Xml.NewLineHandling]::None + + $memStream2 = New-Object System.IO.MemoryStream + $writer2 = [System.Xml.XmlWriter]::Create($memStream2, $settings2) + $objDoc.Save($writer2) + $writer2.Flush(); $writer2.Close() + + $bytes2 = $memStream2.ToArray() + $memStream2.Close() + $text2 = [System.Text.Encoding]::UTF8.GetString($bytes2) + if ($text2.Length -gt 0 -and $text2[0] -eq [char]0xFEFF) { $text2 = $text2.Substring(1) } + $text2 = $text2.Replace('encoding="utf-8"', 'encoding="UTF-8"') + + $utf8Bom2 = New-Object System.Text.UTF8Encoding($true) + [System.IO.File]::WriteAllText($objFile, $text2, $utf8Bom2) + Info " Registered form in: $objFile" +} + +# --- 10e. Helper: check if object is already borrowed in extension --- +function Test-ObjectBorrowed { + param([string]$typeName, [string]$objName) + + $dirName = $childTypeDirMap[$typeName] + $objFile = Join-Path (Join-Path $extDir $dirName) "${objName}.xml" + return (Test-Path $objFile) +} + +# --- 11. Helper: generate InternalInfo XML --- +function Build-InternalInfoXml { + param([string]$typeName, [string]$objName, [string]$indent) + + $types = $script:generatedTypes[$typeName] + if (-not $types -or $types.Count -eq 0) { + return "${indent}" + } + + $sb = New-Object System.Text.StringBuilder + $sb.AppendLine("${indent}") | Out-Null + + # ExchangePlan: ThisNode UUID before GeneratedTypes + if ($typeName -eq "ExchangePlan") { + $thisNodeUuid = [guid]::NewGuid().ToString() + $sb.AppendLine("${indent}`t${thisNodeUuid}") | Out-Null + } + + foreach ($gt in $types) { + $fullName = "$($gt.prefix).${objName}" + $typeId = [guid]::NewGuid().ToString() + $valueId = [guid]::NewGuid().ToString() + $sb.AppendLine("${indent}`t") | Out-Null + $sb.AppendLine("${indent}`t`t${typeId}") | Out-Null + $sb.AppendLine("${indent}`t`t${valueId}") | Out-Null + $sb.AppendLine("${indent}`t") | Out-Null + } + + $sb.Append("${indent}") | Out-Null + return $sb.ToString() +} + +# --- 11b. Collect DataPath references from source Form.xml --- +function Collect-FormDataPaths { + param([string]$formXmlPath) + + $enc = New-Object System.Text.UTF8Encoding($true) + $content = [System.IO.File]::ReadAllText($formXmlPath, $enc) + + $firstLevel = @{} + $deepPaths = @() + + $matches2 = [regex]::Matches($content, '[^<]*\bОбъект\.(\w+(?:\.\w+)*)') + foreach ($m in $matches2) { + $path = $m.Groups[1].Value + $segments = $path.Split(".") + $seg0 = $segments[0] + if ($script:standardFields -contains $seg0) { continue } + $firstLevel[$seg0] = $true + if ($segments.Count -ge 2) { + $seg1 = $segments[1] + if ($script:standardFields -contains $seg1) { continue } + $deepPaths += @{ ObjectAttr = $seg0; SubAttr = $seg1 } + } + } + + # Also collect from TitleDataPath + $matches3 = [regex]::Matches($content, '[^<]*\bОбъект\.(\w+(?:\.\w+)*)') + foreach ($m in $matches3) { + $path = $m.Groups[1].Value + $segments = $path.Split(".") + $seg0 = $segments[0] + if ($script:standardFields -contains $seg0) { continue } + $firstLevel[$seg0] = $true + } + + # Deduplicate deep paths + $seen = @{} + $uniqueDeep = @() + foreach ($dp in $deepPaths) { + $key = "$($dp.ObjectAttr).$($dp.SubAttr)" + if (-not $seen.ContainsKey($key)) { + $seen[$key] = $true + $uniqueDeep += $dp + } + } + + return @{ FirstLevel = $firstLevel; DeepPaths = $uniqueDeep } +} + +# --- 11c. Resolve source attributes and tabular sections --- +function Resolve-SourceAttributes { + param([string]$typeName, [string]$objName, $firstLevelNames) + # $firstLevelNames: hashtable of names, or $null for "all" + + $dirName = $childTypeDirMap[$typeName] + $srcFile = Join-Path (Join-Path $cfgDir $dirName) "${objName}.xml" + if (-not (Test-Path $srcFile)) { + Write-Error "Source object not found: $srcFile" + exit 1 + } + + $srcDoc = New-Object System.Xml.XmlDocument + $srcDoc.PreserveWhitespace = $false + $srcDoc.Load($srcFile) + + $srcNs = New-Object System.Xml.XmlNamespaceManager($srcDoc.NameTable) + $srcNs.AddNamespace("md", $script:mdNs) + $srcNs.AddNamespace("xr", $script:xrNs) + $srcNs.AddNamespace("v8", $script:v8Ns) + + $srcEl = $null + foreach ($c in $srcDoc.DocumentElement.ChildNodes) { + if ($c.NodeType -eq 'Element') { $srcEl = $c; break } + } + if (-not $srcEl) { Write-Error "No metadata element in source: $srcFile"; exit 1 } + + $childObjs = $srcEl.SelectSingleNode("md:ChildObjects", $srcNs) + if (-not $childObjs) { return @{ Attributes = @(); TabularSections = @(); ExtraProps = @{} } } + + $attrs = @() + $tabSections = @() + + foreach ($child in $childObjs.ChildNodes) { + if ($child.NodeType -ne 'Element') { continue } + + if ($child.LocalName -eq 'Attribute') { + $nameNode = $child.SelectSingleNode("md:Properties/md:Name", $srcNs) + if (-not $nameNode) { continue } + $attrName = $nameNode.InnerText + if ($null -ne $firstLevelNames -and -not $firstLevelNames.ContainsKey($attrName)) { continue } + + $uuid = $child.GetAttribute("uuid") + $typeNode = $child.SelectSingleNode("md:Properties/md:Type", $srcNs) + $typeXml = if ($typeNode) { $typeNode.OuterXml } else { "" } + # Strip namespace declarations from Type + $typeXml = [regex]::Replace($typeXml, '\s+xmlns(?::\w+)?="[^"]*"', '') + + $attrs += @{ Name = $attrName; Uuid = $uuid; TypeXml = $typeXml } + } + elseif ($child.LocalName -eq 'TabularSection') { + $nameNode = $child.SelectSingleNode("md:Properties/md:Name", $srcNs) + if (-not $nameNode) { continue } + $tsName = $nameNode.InnerText + if ($null -ne $firstLevelNames -and -not $firstLevelNames.ContainsKey($tsName)) { continue } + + $tsUuid = $child.GetAttribute("uuid") + + # Extract GeneratedTypes from InternalInfo + $tsGenTypes = @() + $iiNode = $child.SelectSingleNode("md:InternalInfo", $srcNs) + if ($iiNode) { + $gtNodes = $iiNode.SelectNodes("xr:GeneratedType", $srcNs) + foreach ($gt in $gtNodes) { + $tsGenTypes += @{ + Name = $gt.GetAttribute("name") + Category = $gt.GetAttribute("category") + TypeId = $gt.SelectSingleNode("xr:TypeId", $srcNs).InnerText + ValueId = $gt.SelectSingleNode("xr:ValueId", $srcNs).InnerText + } + } + } + + # Extract ALL child attributes of TabularSection + $tsAttrs = @() + $tsChildObjs = $child.SelectSingleNode("md:ChildObjects", $srcNs) + if ($tsChildObjs) { + foreach ($tsChild in $tsChildObjs.ChildNodes) { + if ($tsChild.NodeType -ne 'Element' -or $tsChild.LocalName -ne 'Attribute') { continue } + $tsAttrName = $tsChild.SelectSingleNode("md:Properties/md:Name", $srcNs) + if (-not $tsAttrName) { continue } + $tsAttrUuid = $tsChild.GetAttribute("uuid") + $tsTypeNode = $tsChild.SelectSingleNode("md:Properties/md:Type", $srcNs) + $tsTypeXml = if ($tsTypeNode) { $tsTypeNode.OuterXml } else { "" } + $tsTypeXml = [regex]::Replace($tsTypeXml, '\s+xmlns(?::\w+)?="[^"]*"', '') + $tsAttrs += @{ Name = $tsAttrName.InnerText; Uuid = $tsAttrUuid; TypeXml = $tsTypeXml } + } + } + + $tabSections += @{ Name = $tsName; Uuid = $tsUuid; GeneratedTypes = $tsGenTypes; Attributes = $tsAttrs } + } + } + + # Extract extra Properties for main object enrichment (Hierarchical, CodeLength, etc.) + $extraProps = @{} + $propsNode = $srcEl.SelectSingleNode("md:Properties", $srcNs) + if ($propsNode) { + $propsToExtract = @("Hierarchical","FoldersOnTop","CodeLength","DescriptionLength","CodeType","CodeAllowedLength", + "NumberType","NumberLength","NumberAllowedLength","NumberPeriodicity") + foreach ($pName in $propsToExtract) { + $pNode = $propsNode.SelectSingleNode("md:${pName}", $srcNs) + if ($pNode) { $extraProps[$pName] = $pNode.InnerText } + } + } + + return @{ Attributes = $attrs; TabularSections = $tabSections; ExtraProps = $extraProps } +} + +# --- 11d. Build adopted attribute XML --- +function Build-AdoptedAttributeXml { + param([string]$name, [string]$sourceUuid, [string]$typeXml, [string]$indent) + + $newUuid = [guid]::NewGuid().ToString() + $sb = New-Object System.Text.StringBuilder + $sb.AppendLine("${indent}") | Out-Null + $sb.AppendLine("${indent}`t") | Out-Null + $sb.AppendLine("${indent}`t") | Out-Null + $sb.AppendLine("${indent}`t`tAdopted") | Out-Null + $sb.AppendLine("${indent}`t`t${name}") | Out-Null + $sb.AppendLine("${indent}`t`t") | Out-Null + $sb.AppendLine("${indent}`t`t${sourceUuid}") | Out-Null + $sb.AppendLine("${indent}`t`t${typeXml}") | Out-Null + $sb.AppendLine("${indent}`t") | Out-Null + $sb.Append("${indent}") | Out-Null + return $sb.ToString() +} + +# --- 11e. Build adopted tabular section XML --- +function Build-AdoptedTabularSectionXml { + param([string]$tsName, [string]$sourceUuid, $generatedTypes, $childAttrs, [string]$indent) + + $newUuid = [guid]::NewGuid().ToString() + $sb = New-Object System.Text.StringBuilder + $sb.AppendLine("${indent}") | Out-Null + + # InternalInfo with GeneratedTypes (new UUIDs, referencing source names) + if ($generatedTypes -and $generatedTypes.Count -gt 0) { + $sb.AppendLine("${indent}`t") | Out-Null + foreach ($gt in $generatedTypes) { + $newTid = [guid]::NewGuid().ToString() + $newVid = [guid]::NewGuid().ToString() + $sb.AppendLine("${indent}`t`t") | Out-Null + $sb.AppendLine("${indent}`t`t`t${newTid}") | Out-Null + $sb.AppendLine("${indent}`t`t`t${newVid}") | Out-Null + $sb.AppendLine("${indent}`t`t") | Out-Null + } + $sb.AppendLine("${indent}`t") | Out-Null + } else { + $sb.AppendLine("${indent}`t") | Out-Null + } + + $sb.AppendLine("${indent}`t") | Out-Null + $sb.AppendLine("${indent}`t`tAdopted") | Out-Null + $sb.AppendLine("${indent}`t`t${tsName}") | Out-Null + $sb.AppendLine("${indent}`t`t") | Out-Null + $sb.AppendLine("${indent}`t`t${sourceUuid}") | Out-Null + $sb.AppendLine("${indent}`t") | Out-Null + + # ChildObjects with all attributes + if ($childAttrs -and $childAttrs.Count -gt 0) { + $sb.AppendLine("${indent}`t") | Out-Null + foreach ($ca in $childAttrs) { + $caXml = Build-AdoptedAttributeXml $ca.Name $ca.Uuid $ca.TypeXml "${indent}`t`t" + $sb.AppendLine($caXml) | Out-Null + } + $sb.AppendLine("${indent}`t") | Out-Null + } else { + $sb.AppendLine("${indent}`t") | Out-Null + } + + $sb.Append("${indent}") | Out-Null + return $sb.ToString() +} + +# --- 11f. Collect reference types from attribute Type XML strings --- +function Collect-ReferenceTypes { + param([string[]]$typeXmls) + + $result = @{} + foreach ($typeXml in $typeXmls) { + # cfg:CatalogRef.XXX, cfg:EnumRef.XXX, cfg:DocumentRef.XXX, etc. + $refMatches = [regex]::Matches($typeXml, 'cfg:(\w+)Ref\.(\w+)') + foreach ($m in $refMatches) { + $refPrefix = $m.Groups[1].Value # e.g. "Catalog", "Enum", "Document" + $objName = $m.Groups[2].Value + $key = "${refPrefix}.${objName}" + if (-not $result.ContainsKey($key)) { + $result[$key] = @{ TypeName = $refPrefix; ObjName = $objName } + } + } + # cfg:DefinedType.XXX (via v8:TypeSet or v8:Type) + $dtMatches = [regex]::Matches($typeXml, 'cfg:DefinedType\.(\w+)') + foreach ($m in $dtMatches) { + $dtName = $m.Groups[1].Value + $key = "DefinedType.${dtName}" + if (-not $result.ContainsKey($key)) { + $result[$key] = @{ TypeName = "DefinedType"; ObjName = $dtName } + } + } + } + return @($result.Values) +} + +# --- 11g. Merge adopted attributes into existing extension object XML --- +function Merge-AttributesIntoObject { + param([string]$typeName, [string]$objName, $attrsToAdd) + + $dirName = $childTypeDirMap[$typeName] + $objFile = Join-Path (Join-Path $extDir $dirName) "${objName}.xml" + if (-not (Test-Path $objFile)) { + Warn "Cannot merge attributes: $objFile not found" + return + } + + $objDoc = New-Object System.Xml.XmlDocument + $objDoc.PreserveWhitespace = $true + $objDoc.Load($objFile) + + $objNs = New-Object System.Xml.XmlNamespaceManager($objDoc.NameTable) + $objNs.AddNamespace("md", $script:mdNs) + + $objEl = $null + foreach ($c in $objDoc.DocumentElement.ChildNodes) { + if ($c.NodeType -eq 'Element') { $objEl = $c; break } + } + if (-not $objEl) { Warn "No type element in $objFile"; return } + + $childObjs = $objEl.SelectSingleNode("md:ChildObjects", $objNs) + if (-not $childObjs) { + $childObjs = $objDoc.CreateElement("ChildObjects", $script:mdNs) + $objEl.AppendChild($objDoc.CreateWhitespace("`r`n`t`t")) | Out-Null + $objEl.AppendChild($childObjs) | Out-Null + $objEl.AppendChild($objDoc.CreateWhitespace("`r`n`t")) | Out-Null + } + + # Collect existing attribute names for dedup + $existingNames = @{} + foreach ($c in $childObjs.ChildNodes) { + if ($c.NodeType -ne 'Element' -or $c.LocalName -ne 'Attribute') { continue } + $nameNode = $c.SelectSingleNode("md:Properties/md:Name", $objNs) + if ($nameNode) { $existingNames[$nameNode.InnerText] = $true } + } + + $added = 0 + foreach ($attr in $attrsToAdd) { + if ($existingNames.ContainsKey($attr.Name)) { continue } + $attrXml = Build-AdoptedAttributeXml $attr.Name $attr.Uuid $attr.TypeXml "`t`t`t" + + # Expand self-closing ChildObjects if needed + if (-not $childObjs.HasChildNodes -or $childObjs.IsEmpty) { + $closeWs = $objDoc.CreateWhitespace("`r`n`t`t") + $childObjs.AppendChild($closeWs) | Out-Null + } + + $added++ + } + + if ($added -gt 0) { + # Build all adopted attributes as text and do string-level insertion + $allAttrXml = "" + foreach ($attr in $attrsToAdd) { + if ($existingNames.ContainsKey($attr.Name)) { continue } + $allAttrXml += "`r`n" + (Build-AdoptedAttributeXml $attr.Name $attr.Uuid $attr.TypeXml "`t`t`t") + } + + # Save via text manipulation to avoid namespace issues with InnerXml + $settings3 = New-Object System.Xml.XmlWriterSettings + $settings3.Encoding = New-Object System.Text.UTF8Encoding($true) + $settings3.Indent = $false + $settings3.NewLineHandling = [System.Xml.NewLineHandling]::None + $memStream3 = New-Object System.IO.MemoryStream + $writer3 = [System.Xml.XmlWriter]::Create($memStream3, $settings3) + $objDoc.Save($writer3) + $writer3.Flush(); $writer3.Close() + $bytes3 = $memStream3.ToArray() + $memStream3.Close() + $text3 = [System.Text.Encoding]::UTF8.GetString($bytes3) + if ($text3.Length -gt 0 -and $text3[0] -eq [char]0xFEFF) { $text3 = $text3.Substring(1) } + $text3 = $text3.Replace('encoding="utf-8"', 'encoding="UTF-8"') + + # Insert attributes before
+ $text3 = $text3 -replace '
', "${allAttrXml}`r`n`t`t
" + + $utf8Bom3 = New-Object System.Text.UTF8Encoding($true) + [System.IO.File]::WriteAllText($objFile, $text3, $utf8Bom3) + Info " Merged $added attribute(s) into: $objFile" + } +} + +# --- 11h. Borrow-MainAttribute orchestrator --- +function Borrow-MainAttribute { + param([string]$typeName, [string]$objName, [string]$formName, [string]$mode) + + $dirName = $childTypeDirMap[$typeName] + Info "Borrowing main attribute for ${typeName}.${objName} (mode: $mode)..." + + # Step 1: Collect DataPaths (Form mode) or take all (All mode) + $firstLevelNames = $null + $deepPaths = @() + if ($mode -eq "Form") { + $srcFormXmlPath = Join-Path (Join-Path (Join-Path (Join-Path (Join-Path $cfgDir $dirName) $objName) "Forms") $formName) "Ext/Form.xml" + if (-not (Test-Path $srcFormXmlPath)) { + Write-Error "Source Form.xml not found: $srcFormXmlPath" + exit 1 + } + $dp = Collect-FormDataPaths $srcFormXmlPath + $firstLevelNames = $dp.FirstLevel + $deepPaths = $dp.DeepPaths + Info " Collected $($firstLevelNames.Count) first-level DataPath references, $($deepPaths.Count) deep paths" + } else { + Info " Mode All: borrowing all attributes and tabular sections" + } + + # Step 2: Resolve source attributes + $resolved = Resolve-SourceAttributes $typeName $objName $firstLevelNames + $srcAttrs = $resolved.Attributes + $srcTS = $resolved.TabularSections + $extraProps = $resolved.ExtraProps + Info " Resolved: $($srcAttrs.Count) attributes, $($srcTS.Count) tabular section(s)" + + # Identify which FirstLevel names are TabularSections (for deep path filtering) + $tsNames = @{} + foreach ($ts in $srcTS) { $tsNames[$ts.Name] = $true } + + # Step 3: Build the adopted content and insert into main object XML + $objFile = Join-Path (Join-Path $extDir $dirName) "${objName}.xml" + + # Generate full object XML with attributes and TS + $contentSb = New-Object System.Text.StringBuilder + foreach ($attr in $srcAttrs) { + $attrXml = Build-AdoptedAttributeXml $attr.Name $attr.Uuid $attr.TypeXml "`t`t`t" + $contentSb.AppendLine($attrXml) | Out-Null + } + foreach ($ts in $srcTS) { + $tsXml = Build-AdoptedTabularSectionXml $ts.Name $ts.Uuid $ts.GeneratedTypes $ts.Attributes "`t`t`t" + $contentSb.AppendLine($tsXml) | Out-Null + } + $adoptedContent = $contentSb.ToString().TrimEnd() + + # Read existing object XML and inject + $objContent = [System.IO.File]::ReadAllText($objFile, (New-Object System.Text.UTF8Encoding($true))) + + # Inject extra properties after ExtendedConfigurationObject + if ($extraProps.Count -gt 0) { + $propsSb = New-Object System.Text.StringBuilder + foreach ($pName in $extraProps.Keys) { + $propsSb.Append("`r`n`t`t`t<${pName}>$($extraProps[$pName])") | Out-Null + } + $objContent = $objContent -replace '()', "`$1$($propsSb.ToString())" + } + + # Replace empty ChildObjects with adopted content + if ($adoptedContent) { + # Handle (self-closing) + if ($objContent -match '') { + $objContent = $objContent -replace '', "`r`n${adoptedContent}`r`n`t`t" + } + # Handle ... (may already have Form entry) + elseif ($objContent -match '(?s)(.*?)') { + $existingInner = $Matches[1] + $objContent = $objContent -replace '(?s)(.*?)', "${existingInner}`r`n${adoptedContent}`r`n`t`t" + } + } + + $encBom = New-Object System.Text.UTF8Encoding($true) + [System.IO.File]::WriteAllText($objFile, $objContent, $encBom) + Info " Enriched object: $objFile" + + # Step 4: Collect all reference types and borrow as shells + $allTypeXmls = @() + foreach ($a in $srcAttrs) { $allTypeXmls += $a.TypeXml } + foreach ($ts in $srcTS) { + foreach ($tsa in $ts.Attributes) { $allTypeXmls += $tsa.TypeXml } + } + $refTypes = Collect-ReferenceTypes $allTypeXmls + Info " Reference types to borrow: $($refTypes.Count)" + + foreach ($rt in $refTypes) { + if (-not $childTypeDirMap.ContainsKey($rt.TypeName)) { + Warn " Unknown reference type: $($rt.TypeName).$($rt.ObjName)" + continue + } + if (Test-ObjectBorrowed $rt.TypeName $rt.ObjName) { + Info " Already borrowed: $($rt.TypeName).$($rt.ObjName)" + continue + } + $rtSrcFile = Join-Path (Join-Path $cfgDir $childTypeDirMap[$rt.TypeName]) "$($rt.ObjName).xml" + if (-not (Test-Path $rtSrcFile)) { + Warn " Source not found: $($rt.TypeName).$($rt.ObjName)" + continue + } + $src = Read-SourceObject $rt.TypeName $rt.ObjName + $borrowedXml = Build-BorrowedObjectXml $rt.TypeName $rt.ObjName $src.Uuid $src.Properties + $targetDir = Join-Path $extDir $childTypeDirMap[$rt.TypeName] + if (-not (Test-Path $targetDir)) { + New-Item -ItemType Directory -Path $targetDir -Force | Out-Null + } + $targetFile = Join-Path $targetDir "$($rt.ObjName).xml" + [System.IO.File]::WriteAllText($targetFile, $borrowedXml, $encBom) + Add-ToChildObjects $rt.TypeName $rt.ObjName + $script:borrowedFiles += $targetFile + Info " Auto-borrowed: $($rt.TypeName).$($rt.ObjName)" + } + + # Step 5: Handle deep paths (Form mode only) + if ($mode -eq "Form" -and $deepPaths.Count -gt 0) { + # Filter out deep paths where ObjectAttr is a TabularSection (those are TS column refs, not deep attribute refs) + $realDeep = @() + foreach ($dp in $deepPaths) { + if (-not $tsNames.ContainsKey($dp.ObjectAttr)) { $realDeep += $dp } + } + + if ($realDeep.Count -gt 0) { + Info " Processing $($realDeep.Count) deep path(s)..." + + # Group by ObjectAttr → target catalog + $deepByAttr = @{} + foreach ($dp in $realDeep) { + if (-not $deepByAttr.ContainsKey($dp.ObjectAttr)) { $deepByAttr[$dp.ObjectAttr] = @() } + $deepByAttr[$dp.ObjectAttr] += $dp.SubAttr + } + + foreach ($attrName in $deepByAttr.Keys) { + # Find the attribute's type to determine target catalog + $attrInfo = $srcAttrs | Where-Object { $_.Name -eq $attrName } | Select-Object -First 1 + if (-not $attrInfo) { continue } + + # Extract catalog name from type: cfg:CatalogRef.XXX + $catMatch = [regex]::Match($attrInfo.TypeXml, 'cfg:(\w+)Ref\.(\w+)') + if (-not $catMatch.Success) { continue } + + $targetTypeName = $catMatch.Groups[1].Value + $targetObjName = $catMatch.Groups[2].Value + + # Ensure target is borrowed + if (-not (Test-ObjectBorrowed $targetTypeName $targetObjName)) { + $tSrc = Read-SourceObject $targetTypeName $targetObjName + $tBorrowedXml = Build-BorrowedObjectXml $targetTypeName $targetObjName $tSrc.Uuid $tSrc.Properties + $tTargetDir = Join-Path $extDir $childTypeDirMap[$targetTypeName] + if (-not (Test-Path $tTargetDir)) { + New-Item -ItemType Directory -Path $tTargetDir -Force | Out-Null + } + $tTargetFile = Join-Path $tTargetDir "${targetObjName}.xml" + [System.IO.File]::WriteAllText($tTargetFile, $tBorrowedXml, $encBom) + Add-ToChildObjects $targetTypeName $targetObjName + $script:borrowedFiles += $tTargetFile + Info " Auto-borrowed for deep path: ${targetTypeName}.${targetObjName}" + } + + # Resolve sub-attributes in target catalog + $subNames = @{} + foreach ($sn in $deepByAttr[$attrName]) { $subNames[$sn] = $true } + $subResolved = Resolve-SourceAttributes $targetTypeName $targetObjName $subNames + + if ($subResolved.Attributes.Count -gt 0) { + Merge-AttributesIntoObject $targetTypeName $targetObjName $subResolved.Attributes + + # Collect and borrow ref types from deep attributes + $subTypeXmls = @() + foreach ($sa in $subResolved.Attributes) { $subTypeXmls += $sa.TypeXml } + $subRefTypes = Collect-ReferenceTypes $subTypeXmls + foreach ($srt in $subRefTypes) { + if (-not $childTypeDirMap.ContainsKey($srt.TypeName)) { continue } + if (Test-ObjectBorrowed $srt.TypeName $srt.ObjName) { continue } + $sSrcFile = Join-Path (Join-Path $cfgDir $childTypeDirMap[$srt.TypeName]) "$($srt.ObjName).xml" + if (-not (Test-Path $sSrcFile)) { continue } + $sSrc = Read-SourceObject $srt.TypeName $srt.ObjName + $sBorrowedXml = Build-BorrowedObjectXml $srt.TypeName $srt.ObjName $sSrc.Uuid $sSrc.Properties + $sTargetDir = Join-Path $extDir $childTypeDirMap[$srt.TypeName] + if (-not (Test-Path $sTargetDir)) { + New-Item -ItemType Directory -Path $sTargetDir -Force | Out-Null + } + $sTargetFile = Join-Path $sTargetDir "$($srt.ObjName).xml" + [System.IO.File]::WriteAllText($sTargetFile, $sBorrowedXml, $encBom) + Add-ToChildObjects $srt.TypeName $srt.ObjName + $script:borrowedFiles += $sTargetFile + Info " Auto-borrowed (deep): $($srt.TypeName).$($srt.ObjName)" + } + } + } + } + } + + Info " Main attribute borrowing complete" +} + +# --- 12. Helper: build borrowed object XML --- +function Build-BorrowedObjectXml { + param( + [string]$typeName, + [string]$objName, + [string]$sourceUuid, + [hashtable]$sourceProps + ) + + $newUuid = [guid]::NewGuid().ToString() + $internalInfoXml = Build-InternalInfoXml $typeName $objName "`t`t" + + $sb = New-Object System.Text.StringBuilder + $sb.AppendLine("") | Out-Null + $sb.AppendLine("") | Out-Null + $sb.AppendLine("`t<${typeName} uuid=`"${newUuid}`">") | Out-Null + + # InternalInfo + $sb.AppendLine($internalInfoXml) | Out-Null + + # Properties + $sb.AppendLine("`t`t") | Out-Null + $sb.AppendLine("`t`t`tAdopted") | Out-Null + $sb.AppendLine("`t`t`t${objName}") | Out-Null + $sb.AppendLine("`t`t`t") | Out-Null + $sb.AppendLine("`t`t`t${sourceUuid}") | Out-Null + + # CommonModule: extra properties from source + if ($typeName -eq "CommonModule") { + foreach ($propName in $commonModuleProps) { + $propVal = "false" + if ($sourceProps.ContainsKey($propName)) { + $propVal = $sourceProps[$propName] + } + $sb.AppendLine("`t`t`t<${propName}>${propVal}") | Out-Null + } + } + + $sb.AppendLine("`t`t") | Out-Null + + # ChildObjects (for types that need it) + if ($typesWithChildObjects -contains $typeName) { + $sb.AppendLine("`t`t") | Out-Null + } + + $sb.AppendLine("`t") | Out-Null + $sb.Append("") | Out-Null + + return $sb.ToString() +} + +# --- 13. Helper: add object to extension ChildObjects --- +function Add-ToChildObjects { + param([string]$typeName, [string]$objName) + + $cfgIndent = Get-ChildIndent $script:cfgEl + + # Expand self-closing ChildObjects if needed + if (-not $script:childObjsEl.HasChildNodes -or $script:childObjsEl.IsEmpty) { + Expand-SelfClosingElement $script:childObjsEl $cfgIndent + } + $childIndent = Get-ChildIndent $script:childObjsEl + + $typeIdx = $script:typeOrder.IndexOf($typeName) + if ($typeIdx -lt 0) { + Write-Error "Unknown type '$typeName' for ChildObjects ordering" + exit 1 + } + + # Dedup check + foreach ($child in $script:childObjsEl.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq $typeName -and $child.InnerText -eq $objName) { + Warn "Already in ChildObjects: ${typeName}.${objName}" + return + } + } + + # Find insertion point: after last element of same type, or before first element of later type + $insertBefore = $null + $lastSameType = $null + + foreach ($child in $script:childObjsEl.ChildNodes) { + if ($child.NodeType -ne 'Element') { continue } + $childTypeIdx = $script:typeOrder.IndexOf($child.LocalName) + if ($childTypeIdx -lt 0) { continue } + + if ($child.LocalName -eq $typeName) { + # Same type -- check alphabetical order + if ($child.InnerText -gt $objName -and -not $insertBefore) { + $insertBefore = $child + } + $lastSameType = $child + } elseif ($childTypeIdx -gt $typeIdx -and -not $insertBefore) { + # First element of a later type -- insert before it + $insertBefore = $child + } + } + + # Create element + $newEl = $script:xmlDoc.CreateElement($typeName, $script:mdNs) + $newEl.InnerText = $objName + + if ($insertBefore) { + Insert-BeforeElement $script:childObjsEl $newEl $insertBefore $childIndent + } else { + Insert-BeforeElement $script:childObjsEl $newEl $null $childIndent + } + + Info "Added to ChildObjects: ${typeName}.${objName}" +} + +# --- 14. Process each item --- +$script:borrowedFiles = @() +$borrowedCount = 0 + +foreach ($item in $items) { + $dotIdx = $item.IndexOf(".") + if ($dotIdx -lt 1) { + Write-Error "Invalid format '${item}', expected 'Type.Name' or 'Type.Name.Form.FormName'" + exit 1 + } + $typeName = $item.Substring(0, $dotIdx) + $remainder = $item.Substring($dotIdx + 1) + + # Resolve Russian synonym to English type name + if ($synonymMap.ContainsKey($typeName)) { $typeName = $synonymMap[$typeName] } + + if (-not $childTypeDirMap.ContainsKey($typeName)) { + Write-Error "Unknown type '${typeName}'" + exit 1 + } + + # Check for .Form. pattern: Type.ObjName.Form.FormName + $formName = $null + $formIdx = $remainder.IndexOf(".Form.") + if ($formIdx -gt 0) { + $objName = $remainder.Substring(0, $formIdx) + $formName = $remainder.Substring($formIdx + 6) # skip ".Form." + } else { + $objName = $remainder + } + + $dirName = $childTypeDirMap[$typeName] + + if ($formName) { + # --- Form borrowing --- + Info "Borrowing form ${typeName}.${objName}.Form.${formName}..." + + # Auto-borrow parent object if not yet borrowed + if (-not (Test-ObjectBorrowed $typeName $objName)) { + Info " Parent object ${typeName}.${objName} not yet borrowed — borrowing first..." + + $src = Read-SourceObject $typeName $objName + Info " Source UUID: $($src.Uuid)" + $borrowedXml = Build-BorrowedObjectXml $typeName $objName $src.Uuid $src.Properties + + $targetDir = Join-Path $extDir $dirName + if (-not (Test-Path $targetDir)) { + New-Item -ItemType Directory -Path $targetDir -Force | Out-Null + } + $targetFile = Join-Path $targetDir "${objName}.xml" + $enc = New-Object System.Text.UTF8Encoding($true) + [System.IO.File]::WriteAllText($targetFile, $borrowedXml, $enc) + Info " Created: $targetFile" + + Add-ToChildObjects $typeName $objName + $script:borrowedFiles += $targetFile + } + + # Borrow the form + $hasBMA = [bool]$BorrowMainAttribute + $formFiles = Borrow-Form $typeName $objName $formName -BorrowMainAttr:$hasBMA + $script:borrowedFiles += $formFiles + $borrowedCount++ + + # Borrow main attribute if requested + if ($hasBMA) { + Borrow-MainAttribute $typeName $objName $formName $BorrowMainAttribute + } + } else { + # --- Object borrowing (existing logic) --- + Info "Borrowing ${typeName}.${objName}..." + + $src = Read-SourceObject $typeName $objName + Info " Source UUID: $($src.Uuid)" + + $borrowedXml = Build-BorrowedObjectXml $typeName $objName $src.Uuid $src.Properties + + $targetDir = Join-Path $extDir $dirName + if (-not (Test-Path $targetDir)) { + New-Item -ItemType Directory -Path $targetDir -Force | Out-Null + } + + $targetFile = Join-Path $targetDir "${objName}.xml" + $enc = New-Object System.Text.UTF8Encoding($true) + [System.IO.File]::WriteAllText($targetFile, $borrowedXml, $enc) + Info " Created: $targetFile" + + Add-ToChildObjects $typeName $objName + + $script:borrowedFiles += $targetFile + $borrowedCount++ + } +} + +# --- 15. Save modified Configuration.xml --- +$settings = New-Object System.Xml.XmlWriterSettings +$settings.Encoding = New-Object System.Text.UTF8Encoding($true) +$settings.Indent = $false +$settings.NewLineHandling = [System.Xml.NewLineHandling]::None + +$memStream = New-Object System.IO.MemoryStream +$writer = [System.Xml.XmlWriter]::Create($memStream, $settings) +$script:xmlDoc.Save($writer) +$writer.Flush(); $writer.Close() + +$bytes = $memStream.ToArray() +$memStream.Close() +$text = [System.Text.Encoding]::UTF8.GetString($bytes) +if ($text.Length -gt 0 -and $text[0] -eq [char]0xFEFF) { $text = $text.Substring(1) } +$text = $text.Replace('encoding="utf-8"', 'encoding="UTF-8"') + +$utf8Bom = New-Object System.Text.UTF8Encoding($true) +[System.IO.File]::WriteAllText($extResolvedPath, $text, $utf8Bom) +Info "Saved: $extResolvedPath" + +# --- 16. Summary --- +Write-Host "" +Write-Host "=== cfe-borrow summary ===" +Write-Host " Extension: $extDir" +Write-Host " Config: $cfgDir" +Write-Host " Borrowed: $borrowedCount object(s)" +foreach ($f in $script:borrowedFiles) { + Write-Host " - $f" +} +exit 0 diff --git a/.codex/skills/cfe-borrow/scripts/cfe-borrow.py b/.codex/skills/cfe-borrow/scripts/cfe-borrow.py new file mode 100644 index 00000000..7ba8ade9 --- /dev/null +++ b/.codex/skills/cfe-borrow/scripts/cfe-borrow.py @@ -0,0 +1,1559 @@ +#!/usr/bin/env python3 +# cfe-borrow v1.3 — 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"}, + ], + "DefinedType": [ + {"prefix": "DefinedType", "category": "DefinedType"}, + ], +} + +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"] + +# Standard system fields to skip when collecting DataPath references +STANDARD_FIELDS = [ + "Code", "Description", "Ref", "Parent", "DeletionMark", + "Predefined", "IsFolder", "LineNumber", "RowsCount", "PredefinedDataName", +] + +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 detect_format_version(d): + while d: + cfg_path = os.path.join(d, "Configuration.xml") + if os.path.isfile(cfg_path): + with open(cfg_path, "r", encoding="utf-8-sig") as f: + head = f.read(2000) + m = re.search(r']+version="(\d+\.\d+)"', head) + if m: + return m.group(1) + parent = os.path.dirname(d) + if parent == d: + break + d = parent + return "2.17" + + +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"", 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 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(): + sys.stdout.reconfigure(encoding="utf-8") + sys.stderr.reconfigure(encoding="utf-8") + 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) + parser.add_argument("-BorrowMainAttribute", nargs="?", const="Form", default=None) + 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) + + format_version = detect_format_version(ext_dir) + + # --- 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}") + + # Module-level list for borrowed files (used by both main loop and borrow_main_attribute) + borrowed_files = [] + + # --- 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}") + + # --- 11b. Collect DataPath references from source Form.xml --- + def collect_form_data_paths(form_xml_path): + with open(form_xml_path, "r", encoding="utf-8-sig") as fh: + content = fh.read() + + first_level = {} + deep_paths = [] + + for m in re.finditer(r'[^<]*\b\u041e\u0431\u044a\u0435\u043a\u0442\.(\w+(?:\.\w+)*)', content): + path = m.group(1) + segments = path.split(".") + seg0 = segments[0] + if seg0 in STANDARD_FIELDS: + continue + first_level[seg0] = True + if len(segments) >= 2: + seg1 = segments[1] + if seg1 in STANDARD_FIELDS: + continue + deep_paths.append({"ObjectAttr": seg0, "SubAttr": seg1}) + + # Also collect from TitleDataPath + for m in re.finditer(r'[^<]*\b\u041e\u0431\u044a\u0435\u043a\u0442\.(\w+(?:\.\w+)*)', content): + path = m.group(1) + segments = path.split(".") + seg0 = segments[0] + if seg0 in STANDARD_FIELDS: + continue + first_level[seg0] = True + + # Deduplicate deep paths + seen = set() + unique_deep = [] + for dp in deep_paths: + key = f"{dp['ObjectAttr']}.{dp['SubAttr']}" + if key not in seen: + seen.add(key) + unique_deep.append(dp) + + return {"FirstLevel": first_level, "DeepPaths": unique_deep} + + # --- 11c. Resolve source attributes and tabular sections --- + def resolve_source_attributes(type_name, obj_name, first_level_names): + # first_level_names: dict of names, or None for "all" + dir_name = CHILD_TYPE_DIR_MAP[type_name] + 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() + + ns_strip = re.compile(r'\s+xmlns(?::\w+)?="[^"]*"') + + 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 in source: {src_file}", file=sys.stderr) + sys.exit(1) + + child_objs = src_el.find(f"{{{MD_NS}}}ChildObjects") + if child_objs is None: + return {"Attributes": [], "TabularSections": [], "ExtraProps": {}} + + attrs = [] + tab_sections = [] + + for child in child_objs: + if not isinstance(child.tag, str): + continue + ln = localname(child) + + if ln == "Attribute": + name_node = child.find(f"{{{MD_NS}}}Properties/{{{MD_NS}}}Name") + if name_node is None: + continue + attr_name = (name_node.text or "").strip() + if first_level_names is not None and attr_name not in first_level_names: + continue + + attr_uuid = child.get("uuid", "") + type_node = child.find(f"{{{MD_NS}}}Properties/{{{MD_NS}}}Type") + type_xml = "" + if type_node is not None: + type_xml = etree.tostring(type_node, encoding="unicode") + type_xml = ns_strip.sub("", type_xml) + + attrs.append({"Name": attr_name, "Uuid": attr_uuid, "TypeXml": type_xml}) + + elif ln == "TabularSection": + name_node = child.find(f"{{{MD_NS}}}Properties/{{{MD_NS}}}Name") + if name_node is None: + continue + ts_name = (name_node.text or "").strip() + if first_level_names is not None and ts_name not in first_level_names: + continue + + ts_uuid = child.get("uuid", "") + + # Extract GeneratedTypes from InternalInfo + ts_gen_types = [] + ii_node = child.find(f"{{{MD_NS}}}InternalInfo") + if ii_node is not None: + for gt in ii_node: + if isinstance(gt.tag, str) and localname(gt) == "GeneratedType": + gt_name = gt.get("name", "") + gt_category = gt.get("category", "") + tid_el = gt.find(f"{{{XR_NS}}}TypeId") + vid_el = gt.find(f"{{{XR_NS}}}ValueId") + ts_gen_types.append({ + "Name": gt_name, + "Category": gt_category, + "TypeId": (tid_el.text or "") if tid_el is not None else "", + "ValueId": (vid_el.text or "") if vid_el is not None else "", + }) + + # Extract ALL child attributes of TabularSection + ts_attrs = [] + ts_child_objs = child.find(f"{{{MD_NS}}}ChildObjects") + if ts_child_objs is not None: + for ts_child in ts_child_objs: + if not isinstance(ts_child.tag, str) or localname(ts_child) != "Attribute": + continue + ts_attr_name_el = ts_child.find(f"{{{MD_NS}}}Properties/{{{MD_NS}}}Name") + if ts_attr_name_el is None: + continue + ts_attr_uuid = ts_child.get("uuid", "") + ts_type_node = ts_child.find(f"{{{MD_NS}}}Properties/{{{MD_NS}}}Type") + ts_type_xml = "" + if ts_type_node is not None: + ts_type_xml = etree.tostring(ts_type_node, encoding="unicode") + ts_type_xml = ns_strip.sub("", ts_type_xml) + ts_attrs.append({ + "Name": (ts_attr_name_el.text or "").strip(), + "Uuid": ts_attr_uuid, + "TypeXml": ts_type_xml, + }) + + tab_sections.append({ + "Name": ts_name, "Uuid": ts_uuid, + "GeneratedTypes": ts_gen_types, "Attributes": ts_attrs, + }) + + # Extract extra Properties for main object enrichment + extra_props = {} + props_node = src_el.find(f"{{{MD_NS}}}Properties") + if props_node is not None: + props_to_extract = [ + "Hierarchical", "FoldersOnTop", "CodeLength", "DescriptionLength", + "CodeType", "CodeAllowedLength", "NumberType", "NumberLength", + "NumberAllowedLength", "NumberPeriodicity", + ] + for p_name in props_to_extract: + p_node = props_node.find(f"{{{MD_NS}}}{p_name}") + if p_node is not None: + extra_props[p_name] = (p_node.text or "").strip() + + return {"Attributes": attrs, "TabularSections": tab_sections, "ExtraProps": extra_props} + + # --- 11d. Build adopted attribute XML --- + def build_adopted_attribute_xml(name, source_uuid, type_xml, indent): + new_uuid_val = new_guid() + lines = [ + f'{indent}', + f'{indent}\t', + f'{indent}\t', + f'{indent}\t\tAdopted', + f'{indent}\t\t{name}', + f'{indent}\t\t', + f'{indent}\t\t{source_uuid}', + f'{indent}\t\t{type_xml}', + f'{indent}\t', + f'{indent}', + ] + return "\n".join(lines) + + # --- 11e. Build adopted tabular section XML --- + def build_adopted_tabular_section_xml(ts_name, source_uuid, generated_types, child_attrs, indent): + new_uuid_val = new_guid() + lines = [f'{indent}'] + + # InternalInfo with GeneratedTypes (new UUIDs, referencing source names) + if generated_types: + lines.append(f'{indent}\t') + for gt in generated_types: + new_tid = new_guid() + new_vid = new_guid() + lines.append(f'{indent}\t\t') + lines.append(f'{indent}\t\t\t{new_tid}') + lines.append(f'{indent}\t\t\t{new_vid}') + lines.append(f'{indent}\t\t') + lines.append(f'{indent}\t') + else: + lines.append(f'{indent}\t') + + lines.append(f'{indent}\t') + lines.append(f'{indent}\t\tAdopted') + lines.append(f'{indent}\t\t{ts_name}') + lines.append(f'{indent}\t\t') + lines.append(f'{indent}\t\t{source_uuid}') + lines.append(f'{indent}\t') + + # ChildObjects with all attributes + if child_attrs: + lines.append(f'{indent}\t') + for ca in child_attrs: + ca_xml = build_adopted_attribute_xml(ca["Name"], ca["Uuid"], ca["TypeXml"], f"{indent}\t\t") + lines.append(ca_xml) + lines.append(f'{indent}\t') + else: + lines.append(f'{indent}\t') + + lines.append(f'{indent}') + return "\n".join(lines) + + # --- 11f. Collect reference types from attribute Type XML strings --- + def collect_reference_types(type_xmls): + result = {} + for type_xml in type_xmls: + # cfg:CatalogRef.XXX, cfg:EnumRef.XXX, cfg:DocumentRef.XXX, etc. + for m in re.finditer(r'cfg:(\w+)Ref\.(\w+)', type_xml): + ref_prefix = m.group(1) + obj_n = m.group(2) + key = f"{ref_prefix}.{obj_n}" + if key not in result: + result[key] = {"TypeName": ref_prefix, "ObjName": obj_n} + # cfg:DefinedType.XXX + for m in re.finditer(r'cfg:DefinedType\.(\w+)', type_xml): + dt_name = m.group(1) + key = f"DefinedType.{dt_name}" + if key not in result: + result[key] = {"TypeName": "DefinedType", "ObjName": dt_name} + return list(result.values()) + + # --- 11g. Merge adopted attributes into existing extension object XML --- + def merge_attributes_into_object(type_name, obj_name, attrs_to_add): + 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"Cannot merge attributes: {obj_file} not found") + return + + with open(obj_file, "r", encoding="utf-8-sig") as fh: + obj_content = fh.read() + + # Collect existing attribute names for dedup (text-based) + existing_names = set() + for m in re.finditer(r'(\w+)', obj_content): + existing_names.add(m.group(1)) + + all_attr_xml = "" + added = 0 + for attr in attrs_to_add: + if attr["Name"] in existing_names: + continue + all_attr_xml += "\r\n" + build_adopted_attribute_xml(attr["Name"], attr["Uuid"], attr["TypeXml"], "\t\t\t") + added += 1 + + if added > 0: + # Insert attributes — handle both and ... + if re.search(r'', obj_content): + obj_content = re.sub(r'', f"{all_attr_xml}\r\n\t\t", obj_content) + else: + obj_content = obj_content.replace("", f"{all_attr_xml}\r\n\t\t") + save_text_bom(obj_file, obj_content) + info(f" Merged {added} attribute(s) into: {obj_file}") + + # --- 11h. Borrow main attribute orchestrator --- + def borrow_main_attribute(type_name, obj_name, form_name, mode): + dir_name = CHILD_TYPE_DIR_MAP[type_name] + info(f"Borrowing main attribute for {type_name}.{obj_name} (mode: {mode})...") + + # Step 1: Collect DataPaths (Form mode) or take all (All mode) + first_level_names = None + deep_paths = [] + if mode == "Form": + 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) + dp = collect_form_data_paths(src_form_xml_path) + first_level_names = dp["FirstLevel"] + deep_paths = dp["DeepPaths"] + info(f" Collected {len(first_level_names)} first-level DataPath references, {len(deep_paths)} deep paths") + else: + info(" Mode All: borrowing all attributes and tabular sections") + + # Step 2: Resolve source attributes + resolved = resolve_source_attributes(type_name, obj_name, first_level_names) + src_attrs = resolved["Attributes"] + src_ts = resolved["TabularSections"] + extra_props = resolved["ExtraProps"] + info(f" Resolved: {len(src_attrs)} attributes, {len(src_ts)} tabular section(s)") + + # Identify which FirstLevel names are TabularSections (for deep path filtering) + ts_names = {ts["Name"]: True for ts in src_ts} + + # Step 3: Build the adopted content and insert into main object XML + obj_file = os.path.join(ext_dir, dir_name, f"{obj_name}.xml") + + # Generate full object XML with attributes and TS + content_parts = [] + for attr in src_attrs: + attr_xml = build_adopted_attribute_xml(attr["Name"], attr["Uuid"], attr["TypeXml"], "\t\t\t") + content_parts.append(attr_xml) + for ts in src_ts: + ts_xml = build_adopted_tabular_section_xml(ts["Name"], ts["Uuid"], ts["GeneratedTypes"], ts["Attributes"], "\t\t\t") + content_parts.append(ts_xml) + adopted_content = "\n".join(content_parts).rstrip() + + # Read existing object XML and inject + with open(obj_file, "r", encoding="utf-8-sig") as fh: + obj_content = fh.read() + + # Inject extra properties after ExtendedConfigurationObject + if extra_props: + props_xml = "" + for p_name, p_val in extra_props.items(): + props_xml += f"\r\n\t\t\t<{p_name}>{p_val}" + obj_content = obj_content.replace("", f"{props_xml}") + + # Replace empty ChildObjects with adopted content + if adopted_content: + # Handle (self-closing) + if re.search(r'', obj_content): + obj_content = re.sub(r'', f"\r\n{adopted_content}\r\n\t\t", obj_content) + # Handle ... (may already have Form entry) + elif re.search(r'(?s)(.*?)', obj_content): + m = re.search(r'(?s)(.*?)', obj_content) + existing_inner = m.group(1) + obj_content = obj_content.replace( + f"{existing_inner}", + f"{existing_inner}\r\n{adopted_content}\r\n\t\t" + ) + + save_text_bom(obj_file, obj_content) + info(f" Enriched object: {obj_file}") + + # Step 4: Collect all reference types and borrow as shells + all_type_xmls = [] + for a in src_attrs: + all_type_xmls.append(a["TypeXml"]) + for ts in src_ts: + for tsa in ts["Attributes"]: + all_type_xmls.append(tsa["TypeXml"]) + ref_types = collect_reference_types(all_type_xmls) + info(f" Reference types to borrow: {len(ref_types)}") + + for rt in ref_types: + if rt["TypeName"] not in CHILD_TYPE_DIR_MAP: + warn(f" Unknown reference type: {rt['TypeName']}.{rt['ObjName']}") + continue + if test_object_borrowed(rt["TypeName"], rt["ObjName"]): + info(f" Already borrowed: {rt['TypeName']}.{rt['ObjName']}") + continue + rt_src_file = os.path.join(cfg_dir, CHILD_TYPE_DIR_MAP[rt["TypeName"]], f"{rt['ObjName']}.xml") + if not os.path.isfile(rt_src_file): + warn(f" Source not found: {rt['TypeName']}.{rt['ObjName']}") + continue + src = read_source_object(rt["TypeName"], rt["ObjName"]) + borrowed_xml = build_borrowed_object_xml(rt["TypeName"], rt["ObjName"], src["Uuid"], src["Properties"]) + target_dir = os.path.join(ext_dir, CHILD_TYPE_DIR_MAP[rt["TypeName"]]) + os.makedirs(target_dir, exist_ok=True) + target_file = os.path.join(target_dir, f"{rt['ObjName']}.xml") + save_text_bom(target_file, borrowed_xml) + add_to_child_objects(rt["TypeName"], rt["ObjName"]) + borrowed_files.append(target_file) + info(f" Auto-borrowed: {rt['TypeName']}.{rt['ObjName']}") + + # Step 5: Handle deep paths (Form mode only) + if mode == "Form" and deep_paths: + # Filter out deep paths where ObjectAttr is a TabularSection + real_deep = [dp for dp in deep_paths if dp["ObjectAttr"] not in ts_names] + + if real_deep: + info(f" Processing {len(real_deep)} deep path(s)...") + + # Group by ObjectAttr -> target catalog + deep_by_attr = {} + for dp in real_deep: + if dp["ObjectAttr"] not in deep_by_attr: + deep_by_attr[dp["ObjectAttr"]] = [] + deep_by_attr[dp["ObjectAttr"]].append(dp["SubAttr"]) + + for attr_name, sub_attr_names in deep_by_attr.items(): + # Find the attribute's type to determine target catalog + attr_info = None + for a in src_attrs: + if a["Name"] == attr_name: + attr_info = a + break + if not attr_info: + continue + + # Extract catalog name from type: cfg:CatalogRef.XXX + cat_match = re.search(r'cfg:(\w+)Ref\.(\w+)', attr_info["TypeXml"]) + if not cat_match: + continue + + target_type_name = cat_match.group(1) + target_obj_name = cat_match.group(2) + + # Ensure target is borrowed + if not test_object_borrowed(target_type_name, target_obj_name): + t_src = read_source_object(target_type_name, target_obj_name) + t_borrowed_xml = build_borrowed_object_xml(target_type_name, target_obj_name, t_src["Uuid"], t_src["Properties"]) + t_target_dir = os.path.join(ext_dir, CHILD_TYPE_DIR_MAP[target_type_name]) + os.makedirs(t_target_dir, exist_ok=True) + t_target_file = os.path.join(t_target_dir, f"{target_obj_name}.xml") + save_text_bom(t_target_file, t_borrowed_xml) + add_to_child_objects(target_type_name, target_obj_name) + borrowed_files.append(t_target_file) + info(f" Auto-borrowed for deep path: {target_type_name}.{target_obj_name}") + + # Resolve sub-attributes in target catalog + sub_names = {sn: True for sn in sub_attr_names} + sub_resolved = resolve_source_attributes(target_type_name, target_obj_name, sub_names) + + if sub_resolved["Attributes"]: + merge_attributes_into_object(target_type_name, target_obj_name, sub_resolved["Attributes"]) + + # Collect and borrow ref types from deep attributes + sub_type_xmls = [sa["TypeXml"] for sa in sub_resolved["Attributes"]] + sub_ref_types = collect_reference_types(sub_type_xmls) + for srt in sub_ref_types: + if srt["TypeName"] not in CHILD_TYPE_DIR_MAP: + continue + if test_object_borrowed(srt["TypeName"], srt["ObjName"]): + continue + s_src_file = os.path.join(cfg_dir, CHILD_TYPE_DIR_MAP[srt["TypeName"]], f"{srt['ObjName']}.xml") + if not os.path.isfile(s_src_file): + continue + s_src = read_source_object(srt["TypeName"], srt["ObjName"]) + s_borrowed_xml = build_borrowed_object_xml(srt["TypeName"], srt["ObjName"], s_src["Uuid"], s_src["Properties"]) + s_target_dir = os.path.join(ext_dir, CHILD_TYPE_DIR_MAP[srt["TypeName"]]) + os.makedirs(s_target_dir, exist_ok=True) + s_target_file = os.path.join(s_target_dir, f"{srt['ObjName']}.xml") + save_text_bom(s_target_file, s_borrowed_xml) + add_to_child_objects(srt["TypeName"], srt["ObjName"]) + borrowed_files.append(s_target_file) + info(f" Auto-borrowed (deep): {srt['TypeName']}.{srt['ObjName']}") + + info(" Main attribute borrowing complete") + + def borrow_form(type_name, obj_name, form_name, borrow_main_attr=False): + 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", format_version) + + src_auto_cmd = None + form_props = [] + reached_visual = False + 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: + reached_visual = True + src_auto_cmd = fc + continue + if ln in ("ChildItems", "Events", "Attributes", "Commands", "Parameters", "CommandSet"): + reached_visual = True + continue + if not reached_visual: + # Form-level properties before AutoCommandBar (WindowOpeningMode, AutoFillCheck, etc.) + form_props.append(etree.tostring(fc, encoding="unicode")) + + ns_strip_pattern = re.compile(r'\s+xmlns(?::\w+)?="[^"]*"') + + # AutoCommandBar: keep ChildItems (buttons with CommandName->0), Autofill->false + 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') + # Strip ExcludedCommand (references to standard commands invalid in extension) + auto_cmd_xml = re.sub(r'\s*[^<]*', '', auto_cmd_xml) + # Strip DataPath in AutoCommandBar buttons + if borrow_main_attr: + # Keep only Объект.* DataPaths + auto_cmd_xml = re.sub(r'\s*(?!\u041e\u0431\u044a\u0435\u043a\u0442\.)[^<]*', '', auto_cmd_xml) + else: + auto_cmd_xml = re.sub(r'\s*[^<]*', '', auto_cmd_xml) + + # ChildItems: copy full tree, clean up base-config references + child_items_xml = "" + src_child_items = None + for fc in src_form_el: + if isinstance(fc.tag, str) and localname(fc) == "ChildItems": + src_child_items = fc + break + + 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) + # Replace all CommandName values with 0 + child_items_xml = re.sub(r'[^<]*', '0', child_items_xml) + # Strip DataPath / TitleDataPath / RowPictureDataPath + if borrow_main_attr: + # Keep only Объект.* DataPaths — strip form-attribute DataPaths (not borrowed) + child_items_xml = re.sub(r'\s*(?!\u041e\u0431\u044a\u0435\u043a\u0442\.)[^<]*', '', child_items_xml) + child_items_xml = re.sub(r'\s*(?!\u041e\u0431\u044a\u0435\u043a\u0442\.)[^<]*', '', child_items_xml) + child_items_xml = re.sub(r'\s*[^<]*', '', child_items_xml) + else: + child_items_xml = re.sub(r'\s*[^<]*', '', child_items_xml) + child_items_xml = re.sub(r'\s*[^<]*', '', child_items_xml) + child_items_xml = re.sub(r'\s*[^<]*', '', child_items_xml) + # Strip ExcludedCommand in nested AutoCommandBars (references to standard commands invalid in extension) + child_items_xml = re.sub(r'\s*[^<]*', '', child_items_xml) + # Strip TypeLink blocks with human-readable DataPath (Items.XXX) + child_items_xml = re.sub(r'\s*\s*Items\.[^<]*.*?', '', child_items_xml, flags=re.DOTALL) + # Strip element-level Events + child_items_xml = re.sub(r'\s*.*?', '', child_items_xml, flags=re.DOTALL) + + # Collect CommonPicture references from ChildItems and AutoCommandBar + referenced_pictures = {} + for name in re.findall(r'CommonPicture\.(\w+)', child_items_xml): + referenced_pictures[name] = True + if auto_cmd_xml: + for name in re.findall(r'CommonPicture\.(\w+)', auto_cmd_xml): + referenced_pictures[name] = True + + # Auto-borrow referenced CommonPictures + auto_borrowed_pics = [] + for pic_name in referenced_pictures: + if not test_object_borrowed("CommonPicture", pic_name): + pic_src_file = os.path.join(cfg_dir, "CommonPictures", f"{pic_name}.xml") + if os.path.isfile(pic_src_file): + src = read_source_object("CommonPicture", pic_name) + borrowed_xml = build_borrowed_object_xml("CommonPicture", pic_name, src["Uuid"], src["Properties"]) + target_dir = os.path.join(ext_dir, "CommonPictures") + os.makedirs(target_dir, exist_ok=True) + target_file = os.path.join(target_dir, f"{pic_name}.xml") + save_text_bom(target_file, borrowed_xml) + add_to_child_objects("CommonPicture", pic_name) + auto_borrowed_pics.append(pic_name) + borrowed_files.append(target_file) + info(f" Auto-borrowed: CommonPicture.{pic_name}") + else: + warn(f" CommonPicture.{pic_name} not found in source config — will strip from form") + + # Collect all borrowed CommonPictures for Picture stripping + borrowed_pic_set = set() + for co_child in child_objs_el: + if isinstance(co_child.tag, str) and localname(co_child) == "CommonPicture": + borrowed_pic_set.add((co_child.text or "").strip()) + + # Strip blocks referencing non-borrowed CommonPictures (reverse order) + pic_block_pattern = re.compile(r'\s*\s*CommonPicture\.(\w+).*?', re.DOTALL) + pic_matches = list(pic_block_pattern.finditer(child_items_xml)) + for pm in reversed(pic_matches): + cp_name = pm.group(1) + if cp_name not in borrowed_pic_set: + child_items_xml = child_items_xml[:pm.start()] + child_items_xml[pm.end():] + # Strip StdPicture blocks (except Print) + child_items_xml = re.sub(r'\s*\s*StdPicture\.(?!Print\b)\w+.*?', '', child_items_xml, flags=re.DOTALL) + + # Same Picture strip for AutoCommandBar + if auto_cmd_xml: + ac_pic_matches = list(pic_block_pattern.finditer(auto_cmd_xml)) + for pm in reversed(ac_pic_matches): + cp_name = pm.group(1) + if cp_name not in borrowed_pic_set: + auto_cmd_xml = auto_cmd_xml[:pm.start()] + auto_cmd_xml[pm.end():] + auto_cmd_xml = re.sub(r'\s*\s*StdPicture\.(?!Print\b)\w+.*?', '', auto_cmd_xml, flags=re.DOTALL) + + # Auto-borrow StyleItems referenced in ChildItems + referenced_styles = set() + for m in re.finditer(r'ref="style:(\w+)"[^>]*kind="StyleItem"', child_items_xml): + referenced_styles.add(m.group(1)) + for m in re.finditer(r'>style:(\w+)', child_items_xml): + referenced_styles.add(m.group(1)) + + for style_name in referenced_styles: + if not test_object_borrowed("StyleItem", style_name): + style_src_file = os.path.join(cfg_dir, "StyleItems", f"{style_name}.xml") + if os.path.isfile(style_src_file): + src = read_source_object("StyleItem", style_name) + borrowed_xml = build_borrowed_object_xml("StyleItem", style_name, src["Uuid"], src["Properties"]) + target_dir = os.path.join(ext_dir, "StyleItems") + os.makedirs(target_dir, exist_ok=True) + target_file = os.path.join(target_dir, f"{style_name}.xml") + save_text_bom(target_file, borrowed_xml) + add_to_child_objects("StyleItem", style_name) + borrowed_files.append(target_file) + info(f" Auto-borrowed: StyleItem.{style_name}") + else: + warn(f" StyleItem.{style_name} not found in source config") + + # Auto-borrow Enums + EnumValues referenced via DesignTimeRef + referenced_enum_values = {} # enum_name -> set of value_names + for m in re.finditer(r'xr:DesignTimeRef">Enum\.(\w+)\.EnumValue\.(\w+)', child_items_xml): + e_name, ev_name = m.group(1), m.group(2) + if e_name not in referenced_enum_values: + referenced_enum_values[e_name] = set() + referenced_enum_values[e_name].add(ev_name) + + for enum_name, needed_values in referenced_enum_values.items(): + if not test_object_borrowed("Enum", enum_name): + enum_src_file = os.path.join(cfg_dir, "Enums", f"{enum_name}.xml") + if os.path.isfile(enum_src_file): + # Read source Enum to find EnumValue UUIDs + src_enum_tree = etree.parse(enum_src_file, etree.XMLParser(remove_blank_text=False)) + src_enum_root = src_enum_tree.getroot() + src_enum_el = None + for cn in src_enum_root: + if isinstance(cn.tag, str): + src_enum_el = cn + break + + # Find needed EnumValues + ev_xmls = [] + for ev_node in src_enum_el.iter(): + if isinstance(ev_node.tag, str) and localname(ev_node) == "EnumValue": + ev_uuid = ev_node.get("uuid", "") + name_el = None + for props in ev_node: + if isinstance(props.tag, str) and localname(props) == "Properties": + for prop in props: + if isinstance(prop.tag, str) and localname(prop) == "Name": + name_el = prop + break + if name_el is not None and (name_el.text or "").strip() in needed_values: + new_ev_uuid = str(uuid.uuid4()) + ev_xmls.append( + f'\t\t\t\n' + f'\t\t\t\t\n' + f'\t\t\t\t\n' + f'\t\t\t\t\tAdopted\n' + f'\t\t\t\t\t{name_el.text.strip()}\n' + f'\t\t\t\t\t\n' + f'\t\t\t\t\t{ev_uuid}\n' + f'\t\t\t\t\n' + f'\t\t\t' + ) + + # Build borrowed Enum with EnumValues + src_obj = read_source_object("Enum", enum_name) + borrowed_xml = build_borrowed_object_xml("Enum", enum_name, src_obj["Uuid"], src_obj["Properties"]) + if ev_xmls: + ev_block = "\n".join(ev_xmls) + borrowed_xml = borrowed_xml.replace("", f"\n{ev_block}\n\t\t") + + target_dir = os.path.join(ext_dir, "Enums") + os.makedirs(target_dir, exist_ok=True) + target_file = os.path.join(target_dir, f"{enum_name}.xml") + save_text_bom(target_file, borrowed_xml) + add_to_child_objects("Enum", enum_name) + borrowed_files.append(target_file) + info(f" Auto-borrowed: Enum.{enum_name} (with {len(ev_xmls)} EnumValue(s))") + else: + warn(f" Enum.{enum_name} not found in source config") + + # Extract the
opening tag from source text + 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") + + # Part 1: form properties + AutoCommandBar + ChildItems + for prop_xml in form_props: + prop_xml_clean = ns_strip_pattern.sub("", prop_xml) + parts.append(f"\t{prop_xml_clean}\r\n") + if auto_cmd_xml: + parts.append(f"\t{auto_cmd_xml}\r\n") + if child_items_xml: + parts.append(f"\t{child_items_xml}\r\n") + + # Attributes: empty or with MainAttribute when borrow_main_attr + if borrow_main_attr: + obj_type_prefix = "" + gt_list = GENERATED_TYPES.get(type_name, []) + for g in gt_list: + if g["category"] == "Object": + obj_type_prefix = g["prefix"] + break + main_attr_type = f"cfg:{obj_type_prefix}.{obj_name}" + parts.append("\t\r\n") + parts.append('\t\t\r\n') + parts.append(f"\t\t\t{main_attr_type}\r\n") + parts.append("\t\t\ttrue\r\n") + parts.append("\t\t\ttrue\r\n") + parts.append("\t\t\r\n") + parts.append("\t") + else: + parts.append("\t") + parts.append("\r\n") + + # BaseForm: same content, indented one more level + parts.append(f'\t\r\n') + + for prop_xml in form_props: + prop_xml_clean = ns_strip_pattern.sub("", prop_xml) + parts.append(f"\t\t{prop_xml_clean}\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") + if child_items_xml: + 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") + + # BaseForm Attributes: same as main section + if borrow_main_attr: + parts.append("\t\t\r\n") + parts.append('\t\t\t\r\n') + parts.append(f"\t\t\t\t{main_attr_type}\r\n") + parts.append("\t\t\t\ttrue\r\n") + parts.append("\t\t\t\ttrue\r\n") + parts.append("\t\t\t\r\n") + parts.append("\t\t") + else: + parts.append("\t\t") + parts.append("\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) + + # --- 9b. Validate -BorrowMainAttribute --- + borrow_main_attribute_mode = args.BorrowMainAttribute + if borrow_main_attribute_mode is not None: + if borrow_main_attribute_mode not in ("Form", "All"): + print("-BorrowMainAttribute accepts 'Form' or 'All' (default: Form)", file=sys.stderr) + sys.exit(1) + # Validate: only with .Form. pattern + has_form = any(".Form." in item for item in items) + if not has_form: + print("-BorrowMainAttribute requires a form in -Object (e.g. 'Catalog.X.Form.Y')", file=sys.stderr) + sys.exit(1) + + # --- 10. Process each item --- + 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) + + has_bma = borrow_main_attribute_mode is not None + form_files = borrow_form(type_name, obj_name, form_name, borrow_main_attr=has_bma) + borrowed_files.extend(form_files) + borrowed_count += 1 + + # Borrow main attribute if requested + if has_bma: + borrow_main_attribute(type_name, obj_name, form_name, borrow_main_attribute_mode) + 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/.codex/skills/cfe-diff/SKILL.md b/.codex/skills/cfe-diff/SKILL.md new file mode 100644 index 00000000..4b126615 --- /dev/null +++ b/.codex/skills/cfe-diff/SKILL.md @@ -0,0 +1,57 @@ +--- +name: cfe-diff +description: Анализ расширения конфигурации 1С (CFE) — состав, заимствованные объекты, перехватчики, проверка переноса. Используй когда нужно понять что содержит расширение или проверить перенесены ли вставки в конфигурацию +argument-hint: -ExtensionPath -ConfigPath [-Mode A|B] +allowed-tools: + - Bash + - Read + - Glob +--- + +# /cfe-diff — Анализ расширения конфигурации + +Анализирует расширение в двух режимах: обзор изменений (Mode A) или проверка переноса (Mode B). + +## Параметры + +| Параметр | Описание | По умолчанию | +|----------|----------|--------------| +| `ExtensionPath` | Путь к расширению (обязат.) | — | +| `ConfigPath` | Путь к конфигурации (обязат.) | — | +| `Mode` | `A` (обзор) / `B` (проверка переноса) | `A` | + +## Команда + +```powershell +powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/cfe-diff/scripts/cfe-diff.ps1" -ExtensionPath src -ConfigPath C:\cfsrc\erp -Mode A +``` + +## Mode A — обзор расширения + +Для каждого объекта показывает: +- `[BORROWED]` — заимствованный: перехватчики (`&Перед`, `&После`, `&ИзменениеИКонтроль`, `&Вместо`), собственные реквизиты/ТЧ/формы +- `[OWN]` — собственный: количество реквизитов, ТЧ, форм + +Для каждой формы заимствованного объекта показывается: +- `(borrowed)` / `(own)` — заимствованная или собственная форма +- callType-события формы и элементов +- callType на командах + +## Mode B — проверка переноса + +Для каждого `&ИзменениеИКонтроль` извлекает блоки `#Вставка`/`#КонецВставки` из расширения и ищет их в соответствующем модуле конфигурации. + +Статусы: +- `[TRANSFERRED]` — код найден в конфигурации +- `[NOT_TRANSFERRED]` — код не найден +- `[NEEDS_REVIEW]` — нет блоков `#Вставка` или модуль конфигурации не найден + +## Примеры + +```powershell +# Обзор — что изменено в расширении +... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Mode A + +# Проверка переноса — все ли #Вставка перенесены +... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Mode B +``` diff --git a/.codex/skills/cfe-diff/scripts/cfe-diff.ps1 b/.codex/skills/cfe-diff/scripts/cfe-diff.ps1 new file mode 100644 index 00000000..0f6dbdc4 --- /dev/null +++ b/.codex/skills/cfe-diff/scripts/cfe-diff.ps1 @@ -0,0 +1,471 @@ +# cfe-diff v1.0 — Analyze and compare 1C configuration extension (CFE) +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( + [Parameter(Mandatory)] + [string]$ExtensionPath, + + [Parameter(Mandatory)] + [string]$ConfigPath, + + [ValidateSet("A","B")] + [string]$Mode = "A" +) + +$ErrorActionPreference = "Stop" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- Resolve paths --- +if (-not [System.IO.Path]::IsPathRooted($ExtensionPath)) { + $ExtensionPath = Join-Path (Get-Location).Path $ExtensionPath +} +if (-not [System.IO.Path]::IsPathRooted($ConfigPath)) { + $ConfigPath = Join-Path (Get-Location).Path $ConfigPath +} +if (Test-Path $ExtensionPath -PathType Leaf) { $ExtensionPath = Split-Path $ExtensionPath -Parent } +if (Test-Path $ConfigPath -PathType Leaf) { $ConfigPath = Split-Path $ConfigPath -Parent } + +$extCfg = Join-Path $ExtensionPath "Configuration.xml" +$srcCfg = Join-Path $ConfigPath "Configuration.xml" +if (-not (Test-Path $extCfg)) { Write-Error "Extension Configuration.xml not found: $extCfg"; exit 1 } +if (-not (Test-Path $srcCfg)) { Write-Error "Config Configuration.xml not found: $srcCfg"; exit 1 } + +# --- Type -> directory mapping --- +$childTypeDirMap = @{ + "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" +} + +# --- Parse extension Configuration.xml --- +$extDoc = New-Object System.Xml.XmlDocument +$extDoc.PreserveWhitespace = $false +$extDoc.Load($extCfg) + +$ns = New-Object System.Xml.XmlNamespaceManager($extDoc.NameTable) +$ns.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") +$ns.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable") + +$extProps = $extDoc.SelectSingleNode("//md:Configuration/md:Properties", $ns) +$extNameNode = $extProps.SelectSingleNode("md:Name", $ns) +$extName = if ($extNameNode) { $extNameNode.InnerText } else { "?" } +$prefixNode = $extProps.SelectSingleNode("md:NamePrefix", $ns) +$namePrefix = if ($prefixNode -and $prefixNode.InnerText) { $prefixNode.InnerText } else { "" } +$purposeNode = $extProps.SelectSingleNode("md:ConfigurationExtensionPurpose", $ns) +$purpose = if ($purposeNode) { $purposeNode.InnerText } else { "?" } + +Write-Host "=== cfe-diff Mode ${Mode}: $extName (${purpose}) ===" +Write-Host " NamePrefix: $namePrefix" +Write-Host "" + +# --- Collect ChildObjects --- +$childObjNode = $extDoc.SelectSingleNode("//md:Configuration/md:ChildObjects", $ns) +if (-not $childObjNode) { + Write-Host "[WARN] No ChildObjects in extension" + exit 0 +} + +$objects = @() +foreach ($child in $childObjNode.ChildNodes) { + if ($child.NodeType -ne 'Element') { continue } + if ($child.LocalName -eq "Language") { continue } + $objects += @{ Type = $child.LocalName; Name = $child.InnerText } +} + +if ($objects.Count -eq 0) { + Write-Host "No objects (besides Language) in extension." + exit 0 +} + +# --- Helper: check if object is borrowed --- +function Get-ObjectInfo { + param([string]$objType, [string]$objName) + + if (-not $childTypeDirMap.ContainsKey($objType)) { return $null } + $dirName = $childTypeDirMap[$objType] + $objFile = Join-Path (Join-Path $ExtensionPath $dirName) "${objName}.xml" + + if (-not (Test-Path $objFile)) { return @{ Borrowed = $false; File = $objFile; Exists = $false } } + + $doc = New-Object System.Xml.XmlDocument + $doc.PreserveWhitespace = $false + $doc.Load($objFile) + + $objNs = New-Object System.Xml.XmlNamespaceManager($doc.NameTable) + $objNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") + + $objEl = $null + foreach ($c in $doc.DocumentElement.ChildNodes) { + if ($c.NodeType -eq 'Element') { $objEl = $c; break } + } + if (-not $objEl) { return @{ Borrowed = $false; File = $objFile; Exists = $true } } + + $propsEl = $objEl.SelectSingleNode("md:Properties", $objNs) + $obNode = if ($propsEl) { $propsEl.SelectSingleNode("md:ObjectBelonging", $objNs) } else { $null } + + $info = @{ + Borrowed = ($obNode -and $obNode.InnerText -eq "Adopted") + File = $objFile + Exists = $true + Type = $objType + Name = $objName + DirName = $dirName + ObjElement = $objEl + ObjNs = $objNs + } + return $info +} + +# --- Helper: find .bsl files for object --- +function Get-BslFiles { + param([string]$objType, [string]$objName) + + if (-not $childTypeDirMap.ContainsKey($objType)) { return @() } + $dirName = $childTypeDirMap[$objType] + $objDir = Join-Path (Join-Path $ExtensionPath $dirName) $objName + + if (-not (Test-Path $objDir -PathType Container)) { return @() } + + $bslFiles = @() + $extDir = Join-Path $objDir "Ext" + if (Test-Path $extDir) { + $items = Get-ChildItem -Path $extDir -Filter "*.bsl" -ErrorAction SilentlyContinue + foreach ($item in $items) { $bslFiles += $item.FullName } + } + + # Forms + $formsDir = Join-Path $objDir "Forms" + if (Test-Path $formsDir) { + $formModules = Get-ChildItem -Path $formsDir -Recurse -Filter "Module.bsl" -ErrorAction SilentlyContinue + foreach ($fm in $formModules) { $bslFiles += $fm.FullName } + } + + return $bslFiles +} + +# --- Helper: parse interceptors from .bsl --- +function Get-Interceptors { + param([string]$bslPath) + + if (-not (Test-Path $bslPath)) { return @() } + $lines = [System.IO.File]::ReadAllLines($bslPath, [System.Text.Encoding]::UTF8) + $interceptors = @() + $i = 0 + while ($i -lt $lines.Count) { + $line = $lines[$i].Trim() + if ($line -match '^&(Перед|После|ИзменениеИКонтроль|Вместо)\("([^"]+)"\)') { + $type = $Matches[1] + $method = $Matches[2] + $interceptors += @{ Type = $type; Method = $method; Line = $i + 1; File = $bslPath } + } + $i++ + } + return $interceptors +} + +# --- Helper: extract #Вставка blocks from .bsl --- +function Get-InsertionBlocks { + param([string]$bslPath) + + if (-not (Test-Path $bslPath)) { return @() } + $lines = [System.IO.File]::ReadAllLines($bslPath, [System.Text.Encoding]::UTF8) + $blocks = @() + $inBlock = $false + $blockLines = @() + $startLine = 0 + + for ($i = 0; $i -lt $lines.Count; $i++) { + $line = $lines[$i].Trim() + if ($line -eq "#Вставка") { + $inBlock = $true + $blockLines = @() + $startLine = $i + 1 + } elseif ($line -eq "#КонецВставки" -and $inBlock) { + $inBlock = $false + $blocks += @{ + StartLine = $startLine + EndLine = $i + 1 + Code = ($blockLines -join "`n").Trim() + File = $bslPath + } + } elseif ($inBlock) { + $blockLines += $lines[$i] + } + } + return $blocks +} + +# --- Helper: analyze form for callType events and commands --- +function Get-FormInterceptors { + param([string]$formXmlPath) + + if (-not (Test-Path $formXmlPath)) { return $null } + + $formDoc = New-Object System.Xml.XmlDocument + $formDoc.PreserveWhitespace = $false + try { $formDoc.Load($formXmlPath) } catch { return $null } + + $fNs = New-Object System.Xml.XmlNamespaceManager($formDoc.NameTable) + $fNs.AddNamespace("f", "http://v8.1c.ru/8.3/xcf/logform") + + $fRoot = $formDoc.DocumentElement + $baseForm = $fRoot.SelectSingleNode("f:BaseForm", $fNs) + $isBorrowed = ($baseForm -ne $null) + + $interceptors = @() + + # Form-level events with callType + $eventsNode = $fRoot.SelectSingleNode("f:Events", $fNs) + if ($eventsNode) { + foreach ($evt in $eventsNode.SelectNodes("f:Event", $fNs)) { + $ct = $evt.GetAttribute("callType") + if ($ct) { + $interceptors += "Event:$($evt.GetAttribute('name')) [$ct] -> $($evt.InnerText)" + } + } + } + + # Element-level events with callType (scan all elements recursively) + $childItems = $fRoot.SelectSingleNode("f:ChildItems", $fNs) + if ($childItems) { + foreach ($evtNode in $childItems.SelectNodes(".//*[f:Events/f:Event[@callType]]", $fNs)) { + $elName = $evtNode.GetAttribute("name") + foreach ($evt in $evtNode.SelectNodes("f:Events/f:Event[@callType]", $fNs)) { + $ct = $evt.GetAttribute("callType") + $interceptors += "Element:${elName}.$($evt.GetAttribute('name')) [$ct] -> $($evt.InnerText)" + } + } + } + + # Commands with callType on Action + foreach ($cmd in $fRoot.SelectNodes("f:Commands/f:Command", $fNs)) { + $cmdName = $cmd.GetAttribute("name") + foreach ($action in $cmd.SelectNodes("f:Action[@callType]", $fNs)) { + $ct = $action.GetAttribute("callType") + $interceptors += "Command:$cmdName [$ct] -> $($action.InnerText)" + } + } + + return @{ + IsBorrowed = $isBorrowed + Interceptors = $interceptors + } +} + +# ============================================================ +# MODE A: Extension overview +# ============================================================ +if ($Mode -eq "A") { + $borrowedList = @() + $ownList = @() + + foreach ($obj in $objects) { + $info = Get-ObjectInfo $obj.Type $obj.Name + if (-not $info) { + Write-Host " [?] $($obj.Type).$($obj.Name) — unknown type" + continue + } + if (-not $info.Exists) { + Write-Host " [?] $($obj.Type).$($obj.Name) — file not found" + continue + } + + if ($info.Borrowed) { + $borrowedList += $obj + + Write-Host " [BORROWED] $($obj.Type).$($obj.Name)" + + # Find .bsl files and interceptors + $bslFiles = Get-BslFiles $obj.Type $obj.Name + foreach ($bsl in $bslFiles) { + $relPath = $bsl.Replace($ExtensionPath, "").TrimStart("\", "/") + $interceptors = Get-Interceptors $bsl + if ($interceptors.Count -gt 0) { + foreach ($ic in $interceptors) { + Write-Host " &$($ic.Type)(`"$($ic.Method)`") — line $($ic.Line) in $relPath" + } + } else { + Write-Host " $relPath (no interceptors)" + } + } + + # Check for own attributes/forms in ChildObjects + if ($info.ObjElement) { + $childObj = $info.ObjElement.SelectSingleNode("md:ChildObjects", $info.ObjNs) + if ($childObj) { + $ownAttrs = 0 + $ownForms = 0 + $ownTS = 0 + $borrowedItems = 0 + $formNames = @() + foreach ($c in $childObj.ChildNodes) { + if ($c.NodeType -ne 'Element') { continue } + $cProps = $c.SelectSingleNode("md:Properties", $info.ObjNs) + if ($cProps) { + $cOb = $cProps.SelectSingleNode("md:ObjectBelonging", $info.ObjNs) + if ($cOb -and $cOb.InnerText -eq "Adopted") { + $borrowedItems++ + continue + } + } + switch ($c.LocalName) { + "Attribute" { $ownAttrs++ } + "TabularSection" { $ownTS++ } + "Form" { $formNames += $c.InnerText; $ownForms++ } + } + } + $parts = @() + if ($ownAttrs -gt 0) { $parts += "$ownAttrs own attrs" } + if ($ownTS -gt 0) { $parts += "$ownTS own TS" } + if ($ownForms -gt 0) { $parts += "$ownForms own forms" } + if ($borrowedItems -gt 0) { $parts += "$borrowedItems borrowed items" } + if ($parts.Count -gt 0) { + Write-Host " ChildObjects: $($parts -join ', ')" + } + + # Analyze forms + $borrowedFormCount = 0 + $ownFormCount = 0 + foreach ($fn in $formNames) { + $formXmlPath = Join-Path (Join-Path (Join-Path (Join-Path (Join-Path $ExtensionPath $info.DirName) $info.Name) "Forms") $fn) "Ext/Form.xml" + $fi = Get-FormInterceptors $formXmlPath + if (-not $fi) { + Write-Host " Form.$fn (?)" + continue + } + $formTag = if ($fi.IsBorrowed) { "borrowed"; $borrowedFormCount++ } else { "own"; $ownFormCount++ } + if ($fi.Interceptors.Count -gt 0) { + Write-Host " Form.$fn ($formTag):" + foreach ($ic in $fi.Interceptors) { + Write-Host " $ic" + } + } else { + Write-Host " Form.$fn ($formTag)" + } + } + } + } + } else { + $ownList += $obj + Write-Host " [OWN] $($obj.Type).$($obj.Name)" + + # Brief info for own objects + if ($info.ObjElement) { + $childObj = $info.ObjElement.SelectSingleNode("md:ChildObjects", $info.ObjNs) + if ($childObj) { + $attrs = 0; $forms = 0; $ts = 0 + foreach ($c in $childObj.ChildNodes) { + if ($c.NodeType -ne 'Element') { continue } + switch ($c.LocalName) { + "Attribute" { $attrs++ } + "TabularSection" { $ts++ } + "Form" { $forms++ } + } + } + $parts = @() + if ($attrs -gt 0) { $parts += "$attrs attrs" } + if ($ts -gt 0) { $parts += "$ts TS" } + if ($forms -gt 0) { $parts += "$forms forms" } + if ($parts.Count -gt 0) { + Write-Host " $($parts -join ', ')" + } + } + } + } + } + + Write-Host "" + Write-Host "=== Summary: $($borrowedList.Count) borrowed, $($ownList.Count) own objects ===" +} + +# ============================================================ +# MODE B: Transfer check +# ============================================================ +if ($Mode -eq "B") { + $transferred = 0 + $notTransferred = 0 + $needsReview = 0 + + foreach ($obj in $objects) { + $info = Get-ObjectInfo $obj.Type $obj.Name + if (-not $info -or -not $info.Exists -or -not $info.Borrowed) { continue } + + # Find .bsl files with &ИзменениеИКонтроль + $bslFiles = Get-BslFiles $obj.Type $obj.Name + foreach ($bsl in $bslFiles) { + $interceptors = Get-Interceptors $bsl + $macInterceptors = @($interceptors | Where-Object { $_.Type -eq "ИзменениеИКонтроль" }) + + if ($macInterceptors.Count -eq 0) { continue } + + foreach ($ic in $macInterceptors) { + $methodName = $ic.Method + $relBsl = $bsl.Replace($ExtensionPath, "").TrimStart("\", "/") + + # Find #Вставка blocks in this file + $insertBlocks = Get-InsertionBlocks $bsl + + if ($insertBlocks.Count -eq 0) { + Write-Host " [NEEDS_REVIEW] $($obj.Type).$($obj.Name) — &ИзменениеИКонтроль(`"$methodName`") — no #Вставка blocks" + $needsReview++ + continue + } + + # Find corresponding module in config + if (-not $childTypeDirMap.ContainsKey($obj.Type)) { continue } + $dirName = $childTypeDirMap[$obj.Type] + $configBsl = $bsl.Replace($ExtensionPath, $ConfigPath) + + if (-not (Test-Path $configBsl)) { + Write-Host " [NEEDS_REVIEW] $($obj.Type).$($obj.Name) — &ИзменениеИКонтроль(`"$methodName`") — config module not found" + $needsReview++ + continue + } + + $configContent = [System.IO.File]::ReadAllText($configBsl, [System.Text.Encoding]::UTF8) + + $allTransferred = $true + foreach ($block in $insertBlocks) { + $code = $block.Code + if (-not $code) { continue } + + # Normalize whitespace for comparison + $codeNorm = $code -replace '\s+', ' ' + $configNorm = $configContent -replace '\s+', ' ' + + if ($configNorm.Contains($codeNorm)) { + # Found in config + } else { + $allTransferred = $false + } + } + + if ($allTransferred) { + Write-Host " [TRANSFERRED] $($obj.Type).$($obj.Name) — &ИзменениеИКонтроль(`"$methodName`") — $($insertBlocks.Count) block(s)" + $transferred++ + } else { + Write-Host " [NOT_TRANSFERRED] $($obj.Type).$($obj.Name) — &ИзменениеИКонтроль(`"$methodName`") — some blocks not found in config" + $notTransferred++ + } + } + } + } + + Write-Host "" + Write-Host "=== Transfer check: $transferred transferred, $notTransferred not transferred, $needsReview needs review ===" +} diff --git a/.codex/skills/cfe-diff/scripts/cfe-diff.py b/.codex/skills/cfe-diff/scripts/cfe-diff.py new file mode 100644 index 00000000..3398410a --- /dev/null +++ b/.codex/skills/cfe-diff/scripts/cfe-diff.py @@ -0,0 +1,540 @@ +#!/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(): + sys.stdout.reconfigure(encoding="utf-8") + sys.stderr.reconfigure(encoding="utf-8") + 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/.codex/skills/cfe-init/SKILL.md b/.codex/skills/cfe-init/SKILL.md new file mode 100644 index 00000000..26187244 --- /dev/null +++ b/.codex/skills/cfe-init/SKILL.md @@ -0,0 +1,71 @@ +--- +name: cfe-init +description: Создать расширение конфигурации 1С (CFE) — scaffold XML-исходников. Используй когда нужно создать новое расширение для исправления, доработки или дополнения конфигурации +argument-hint: [-ConfigPath ] [-Purpose Patch|Customization|AddOn] [-CompatibilityMode Version8_3_24] +allowed-tools: + - Bash + - Read + - Glob +--- + +# /cfe-init — Создание расширения конфигурации 1С + +Создаёт scaffold расширения: `Configuration.xml`, `Languages/Русский.xml`, опционально `Roles/`. + +## Подготовка + +Если есть выгрузка базовой конфигурации, передай `-ConfigPath` — скрипт автоматически определит `CompatibilityMode` и UUID языка из базовой конфигурации. + +### Авто-определение ConfigPath + +Если пользователь не указал `-ConfigPath` — попробуй определить автоматически: +1. Прочитай `.v8-project.json` из корня проекта +2. Разреши целевую базу (по имени, ветке или `default` — алгоритм из `/db-list`) +3. Если у базы есть поле `configSrc` — используй как `-ConfigPath` +4. Если `configSrc` нет — спроси у пользователя + +Если `.v8-project.json` не найден и `-ConfigPath` не задан — расширение создастся с предупреждением (UUID языка = нули, CompatibilityMode по умолчанию). + +## Параметры + +| Параметр | Описание | По умолчанию | +|----------|----------|--------------| +| `Name` | Имя расширения (обязат.) | — | +| `Synonym` | Синоним | = Name | +| `NamePrefix` | Префикс собственных объектов | = Name + "_" | +| `OutputDir` | Каталог для создания | `src` | +| `Purpose` | `Patch` (исправление) / `Customization` (доработка) / `AddOn` (дополнение) | `Customization` | +| `Version` | Версия расширения | — | +| `Vendor` | Поставщик | — | +| `CompatibilityMode` | Режим совместимости | `Version8_3_24` | +| `ConfigPath` | Путь к выгрузке базовой конфигурации (авто-определяет CompatibilityMode и Language UUID) | — | +| `NoRole` | Без основной роли | false | + +## Команда + +```powershell +powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/cfe-init/scripts/cfe-init.ps1" -Name "МоёРасширение" +``` + +## Примеры + +```powershell +# Расширение для ERP с авто-определением совместимости из базовой конфигурации +... -Name Расш1 -ConfigPath C:\WS\tasks\cfsrc\erp_8.3.24 -OutputDir src + +# Расширение-исправление с явным режимом совместимости +... -Name Расш1 -Purpose Patch -CompatibilityMode Version8_3_17 -OutputDir src + +# Расширение-доработка с версией +... -Name МоёРасширение -Version "1.0.0.1" -Vendor "Компания" -OutputDir src + +# Без роли, с явным префиксом +... -Name ИсправлениеБага -NamePrefix "ИБ_" -Purpose Patch -NoRole -OutputDir src +``` + +## Верификация + +``` +/cfe-validate +``` + diff --git a/.codex/skills/cfe-init/scripts/cfe-init.ps1 b/.codex/skills/cfe-init/scripts/cfe-init.ps1 new file mode 100644 index 00000000..b131abe0 --- /dev/null +++ b/.codex/skills/cfe-init/scripts/cfe-init.ps1 @@ -0,0 +1,270 @@ +# cfe-init v1.1 — Create 1C configuration extension scaffold (CFE) +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( + [Parameter(Mandatory)] + [string]$Name, + [string]$Synonym = $Name, + [string]$NamePrefix, + [string]$OutputDir = "src", + [ValidateSet("Patch","Customization","AddOn")] + [string]$Purpose = "Customization", + [string]$Version, + [string]$Vendor, + [string]$CompatibilityMode = "Version8_3_24", + [string]$ConfigPath, + [switch]$NoRole +) + +$ErrorActionPreference = "Stop" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- Default NamePrefix --- +if (-not $NamePrefix) { + $NamePrefix = "${Name}_" +} + +# --- Resolve output dir --- +if (-not [System.IO.Path]::IsPathRooted($OutputDir)) { + $OutputDir = Join-Path (Get-Location).Path $OutputDir +} + +# --- Check existing --- +$cfgFile = Join-Path $OutputDir "Configuration.xml" +if (Test-Path $cfgFile) { + Write-Error "Configuration.xml already exists: $cfgFile" + exit 1 +} + +# --- Resolve ConfigPath --- +$baseLangUuid = "00000000-0000-0000-0000-000000000000" +if ($ConfigPath) { + if (-not [System.IO.Path]::IsPathRooted($ConfigPath)) { + $ConfigPath = Join-Path (Get-Location).Path $ConfigPath + } + if (Test-Path $ConfigPath -PathType Container) { + $candidate = Join-Path $ConfigPath "Configuration.xml" + if (Test-Path $candidate) { $ConfigPath = $candidate } + else { Write-Error "No Configuration.xml in config directory: $ConfigPath"; exit 1 } + } + if (-not (Test-Path $ConfigPath)) { Write-Error "Config file not found: $ConfigPath"; exit 1 } + $cfgDir = Split-Path (Resolve-Path $ConfigPath).Path -Parent + + # 3a. Read Language UUID from base config + $baseLangFile = Join-Path (Join-Path $cfgDir "Languages") "Русский.xml" + if (Test-Path $baseLangFile) { + $baseLangDoc = New-Object System.Xml.XmlDocument + $baseLangDoc.PreserveWhitespace = $false + $baseLangDoc.Load($baseLangFile) + $langEl = $null + foreach ($c in $baseLangDoc.DocumentElement.ChildNodes) { + if ($c.NodeType -eq 'Element' -and $c.LocalName -eq 'Language') { $langEl = $c; break } + } + if ($langEl) { + $baseLangUuid = $langEl.GetAttribute("uuid") + Write-Host "[INFO] Base config Language UUID: $baseLangUuid" + } else { + Write-Host "[WARN] No element in $baseLangFile" + } + } else { + Write-Host "[WARN] Base config language not found: $baseLangFile" + } + + # 3b. Read CompatibilityMode and InterfaceCompatibilityMode from base config + $baseCfgDoc = New-Object System.Xml.XmlDocument + $baseCfgDoc.PreserveWhitespace = $false + $baseCfgDoc.Load((Resolve-Path $ConfigPath).Path) + $baseCfgNs = New-Object System.Xml.XmlNamespaceManager($baseCfgDoc.NameTable) + $baseCfgNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") + $compatNode = $baseCfgDoc.SelectSingleNode("//md:Configuration/md:Properties/md:CompatibilityMode", $baseCfgNs) + if ($compatNode -and $compatNode.InnerText) { + $CompatibilityMode = $compatNode.InnerText.Trim() + Write-Host "[INFO] Base config CompatibilityMode: $CompatibilityMode" + } else { + Write-Host "[WARN] CompatibilityMode not found in base config, using default: $CompatibilityMode" + } + $ifcNode = $baseCfgDoc.SelectSingleNode("//md:Configuration/md:Properties/md:InterfaceCompatibilityMode", $baseCfgNs) + if ($ifcNode -and $ifcNode.InnerText) { + $InterfaceCompatibilityMode = $ifcNode.InnerText.Trim() + Write-Host "[INFO] Base config InterfaceCompatibilityMode: $InterfaceCompatibilityMode" + } else { + $InterfaceCompatibilityMode = "TaxiEnableVersion8_2" + Write-Host "[WARN] InterfaceCompatibilityMode not found in base config, using default: $InterfaceCompatibilityMode" + } +} else { + $InterfaceCompatibilityMode = "TaxiEnableVersion8_2" + Write-Host "[WARN] Language ExtendedConfigurationObject set to zeros. Use -ConfigPath to auto-resolve from base config, or fix manually before loading." +} + +# --- Generate UUIDs --- +$uuidCfg = [guid]::NewGuid().ToString() +$uuidLang = [guid]::NewGuid().ToString() +$uuidRole = [guid]::NewGuid().ToString() + +# 7 ContainedObject ObjectIds +$co1 = [guid]::NewGuid().ToString() +$co2 = [guid]::NewGuid().ToString() +$co3 = [guid]::NewGuid().ToString() +$co4 = [guid]::NewGuid().ToString() +$co5 = [guid]::NewGuid().ToString() +$co6 = [guid]::NewGuid().ToString() +$co7 = [guid]::NewGuid().ToString() + +# --- Synonym XML --- +$synonymXml = "" +if ($Synonym) { + $synonymXml = "`r`n`t`t`t`t`r`n`t`t`t`t`tru`r`n`t`t`t`t`t$([System.Security.SecurityElement]::Escape($Synonym))`r`n`t`t`t`t`r`n`t`t`t" +} + +# --- Optional properties --- +$vendorXml = if ($Vendor) { [System.Security.SecurityElement]::Escape($Vendor) } else { "" } +$versionXml = if ($Version) { [System.Security.SecurityElement]::Escape($Version) } else { "" } + +# --- Role name --- +$roleName = "${NamePrefix}ОсновнаяРоль" + +# --- DefaultRoles XML --- +$defaultRolesXml = "" +if (-not $NoRole) { + $defaultRolesXml = "`r`n`t`t`t`tRole.$roleName`r`n`t`t`t" +} + +# --- ChildObjects --- +$childObjectsXml = "`r`n`t`t`tРусский" +if (-not $NoRole) { + $childObjectsXml += "`r`n`t`t`t$roleName" +} +$childObjectsXml += "`r`n`t`t" + +# --- Configuration.xml --- +$cfgXml = @" + + + + + + 9cd510cd-abfc-11d4-9434-004095e12fc7 + $co1 + + + 9fcd25a0-4822-11d4-9414-008048da11f9 + $co2 + + + e3687481-0a87-462c-a166-9f34594f9bba + $co3 + + + 9de14907-ec23-4a07-96f0-85521cb6b53b + $co4 + + + 51f2d5d8-ea4d-4064-8892-82951750031e + $co5 + + + e68182ea-4237-4383-967f-90c1e3370bc7 + $co6 + + + fb282519-d103-4dd3-bc12-cb271d631dfc + $co7 + + + + Adopted + $([System.Security.SecurityElement]::Escape($Name)) + $synonymXml + + $Purpose + true + $([System.Security.SecurityElement]::Escape($NamePrefix)) + $CompatibilityMode + ManagedApplication + + PlatformApplication + + Russian + $defaultRolesXml + $vendorXml + $versionXml + Language.Русский + + + + + + $InterfaceCompatibilityMode + + $childObjectsXml + + +"@ + +# --- Languages/Русский.xml (adopted format) --- +$langXml = @" + + + + + + Adopted + Русский + + $baseLangUuid + ru + + + +"@ + +# --- Role XML --- +$roleXml = @" + + + + + $([System.Security.SecurityElement]::Escape($roleName)) + + + + + +"@ + +# --- Create directories --- +if (-not (Test-Path $OutputDir)) { + New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null +} +$langDir = Join-Path $OutputDir "Languages" +if (-not (Test-Path $langDir)) { + New-Item -ItemType Directory -Path $langDir -Force | Out-Null +} + +# --- Write files with UTF-8 BOM --- +$enc = New-Object System.Text.UTF8Encoding($true) + +[System.IO.File]::WriteAllText($cfgFile, $cfgXml, $enc) +$langFile = Join-Path $langDir "Русский.xml" +[System.IO.File]::WriteAllText($langFile, $langXml, $enc) + +# --- Role --- +if (-not $NoRole) { + $roleDir = Join-Path $OutputDir "Roles" + if (-not (Test-Path $roleDir)) { + New-Item -ItemType Directory -Path $roleDir -Force | Out-Null + } + $roleFile = Join-Path $roleDir "$roleName.xml" + [System.IO.File]::WriteAllText($roleFile, $roleXml, $enc) +} + +# --- Output --- +Write-Host "[OK] Создано расширение: $Name" +Write-Host " Каталог: $OutputDir" +Write-Host " Назначение: $Purpose" +Write-Host " Префикс: $NamePrefix" +Write-Host " Совместимость: $CompatibilityMode" +Write-Host " Configuration.xml: $cfgFile" +Write-Host " Languages: $langFile" +if (-not $NoRole) { + Write-Host " Role: $roleFile" +} diff --git a/.codex/skills/cfe-init/scripts/cfe-init.py b/.codex/skills/cfe-init/scripts/cfe-init.py new file mode 100644 index 00000000..a1245f97 --- /dev/null +++ b/.codex/skills/cfe-init/scripts/cfe-init.py @@ -0,0 +1,248 @@ +#!/usr/bin/env python3 +# cfe-init v1.1 — 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(): + sys.stdout.reconfigure(encoding="utf-8") + sys.stderr.reconfigure(encoding="utf-8") + 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 and InterfaceCompatibilityMode 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}") + ifc_node = base_cfg_root.find('.//md:Configuration/md:Properties/md:InterfaceCompatibilityMode', ns) + if ifc_node is not None and ifc_node.text: + ifc_mode = ifc_node.text.strip() + print(f"[INFO] Base config InterfaceCompatibilityMode: {ifc_mode}") + else: + ifc_mode = "TaxiEnableVersion8_2" + print(f"[WARN] InterfaceCompatibilityMode not found in base config, using default: {ifc_mode}") + except Exception: + print(f"[WARN] Could not parse base config, using default CompatibilityMode: {compat}") + ifc_mode = "TaxiEnableVersion8_2" + else: + ifc_mode = "TaxiEnableVersion8_2" + 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\t{ifc_mode} +\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/.codex/skills/cfe-patch-method/SKILL.md b/.codex/skills/cfe-patch-method/SKILL.md new file mode 100644 index 00000000..f63c06e8 --- /dev/null +++ b/.codex/skills/cfe-patch-method/SKILL.md @@ -0,0 +1,78 @@ +--- +name: cfe-patch-method +description: Генерация перехватчика метода в расширении 1С (CFE). Используй когда нужно перехватить метод заимствованного объекта — вставить код до, после или вместо оригинального +argument-hint: -ExtensionPath -ModulePath "Catalog.X.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before +allowed-tools: + - Bash + - Read + - Glob +--- + +# /cfe-patch-method — Генерация перехватчика метода + +Генерирует `.bsl` файл с декоратором перехвата для заимствованного объекта расширения. Создаёт файл или дописывает в существующий. + +## Предусловие + +Объект должен быть заимствован в расширение (`/cfe-borrow`). Скрипт читает `NamePrefix` из `Configuration.xml` расширения для формирования имени процедуры. + +## Параметры + +| Параметр | Описание | По умолчанию | +|----------|----------|--------------| +| `ExtensionPath` | Путь к расширению (обязат.) | — | +| `ModulePath` | Путь к модулю (обязат.) | — | +| `MethodName` | Имя перехватываемого метода (обязат.) | — | +| `InterceptorType` | `Before` / `After` / `ModificationAndControl` (обязат.) | — | +| `Context` | Директива контекста | `НаСервере` | +| `IsFunction` | Метод — функция (добавит `Возврат`) | false | + +## Формат ModulePath + +| ModulePath | Файл | +|------------|------| +| `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.Form.Y` | `Documents/X/Forms/Y/Ext/Form/Module.bsl` | + +Аналогично для Report, DataProcessor, InformationRegister и других типов. + +## Типы перехвата + +| InterceptorType | Декоратор | Назначение | +|-----------------|-----------|------------| +| `Before` | `&Перед` | Код до вызова оригинального метода | +| `After` | `&После` | Код после вызова оригинального метода | +| `ModificationAndControl` | `&ИзменениеИКонтроль` | Копия тела метода с маркерами `#Вставка`/`#Удаление` | + +## Команда + +```powershell +powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/cfe-patch-method/scripts/cfe-patch-method.ps1" -ExtensionPath src -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before +``` + +## Примеры + +```powershell +# Перехват &Перед на сервере +... -ExtensionPath src -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before + +# Перехват &После на клиенте +... -ExtensionPath src -ModulePath "Document.Заказ.Form.ФормаДокумента" -MethodName "ПослеЗаписиНаСервере" -InterceptorType After -Context "НаКлиенте" + +# ИзменениеИКонтроль для функции +... -ExtensionPath src -ModulePath "CommonModule.ОбщийМодуль" -MethodName "ПолучитьДанные" -InterceptorType ModificationAndControl -IsFunction +``` + +## Генерируемый код (Before) + +```bsl +&НаСервере +&Перед("ПриЗаписи") +Процедура Расш1_ПриЗаписи() + // TODO: код перед вызовом оригинального метода +КонецПроцедуры +``` diff --git a/.codex/skills/cfe-patch-method/scripts/cfe-patch-method.ps1 b/.codex/skills/cfe-patch-method/scripts/cfe-patch-method.ps1 new file mode 100644 index 00000000..96324c3b --- /dev/null +++ b/.codex/skills/cfe-patch-method/scripts/cfe-patch-method.ps1 @@ -0,0 +1,209 @@ +# cfe-patch-method v1.1 — Generate method interceptor for 1C extension (CFE) +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( + [Parameter(Mandatory)] + [string]$ExtensionPath, + + [Parameter(Mandatory)] + [string]$ModulePath, + + [Parameter(Mandatory)] + [string]$MethodName, + + [Parameter(Mandatory)] + [ValidateSet("Before","After","ModificationAndControl")] + [string]$InterceptorType, + + [string]$Context = "НаСервере", + + [switch]$IsFunction +) + +$ErrorActionPreference = "Stop" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- Resolve extension path --- +if (-not [System.IO.Path]::IsPathRooted($ExtensionPath)) { + $ExtensionPath = Join-Path (Get-Location).Path $ExtensionPath +} +if (Test-Path $ExtensionPath -PathType Leaf) { + $ExtensionPath = Split-Path $ExtensionPath -Parent +} +$cfgFile = Join-Path $ExtensionPath "Configuration.xml" +if (-not (Test-Path $cfgFile)) { + Write-Error "Configuration.xml not found in: $ExtensionPath" + exit 1 +} + +# --- Read NamePrefix from Configuration.xml --- +$cfgDoc = New-Object System.Xml.XmlDocument +$cfgDoc.PreserveWhitespace = $false +$cfgDoc.Load($cfgFile) + +$cfgNs = New-Object System.Xml.XmlNamespaceManager($cfgDoc.NameTable) +$cfgNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") + +$propsNode = $cfgDoc.SelectSingleNode("//md:Configuration/md:Properties", $cfgNs) +$prefixNode = if ($propsNode) { $propsNode.SelectSingleNode("md:NamePrefix", $cfgNs) } else { $null } +$namePrefix = if ($prefixNode -and $prefixNode.InnerText) { $prefixNode.InnerText } else { "Расш_" } + +# --- 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 + +$typeDirMap = @{ + "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" + "Catalogs"="Catalogs"; "Documents"="Documents"; "Enums"="Enums" + "CommonModules"="CommonModules"; "Reports"="Reports"; "DataProcessors"="DataProcessors" + "ExchangePlans"="ExchangePlans"; "ChartsOfAccounts"="ChartsOfAccounts" + "ChartsOfCharacteristicTypes"="ChartsOfCharacteristicTypes" + "ChartsOfCalculationTypes"="ChartsOfCalculationTypes" + "BusinessProcesses"="BusinessProcesses"; "Tasks"="Tasks" + "InformationRegisters"="InformationRegisters"; "AccumulationRegisters"="AccumulationRegisters" + "AccountingRegisters"="AccountingRegisters"; "CalculationRegisters"="CalculationRegisters" +} + +$parts = $ModulePath.Split(".") +if ($parts.Count -lt 2) { + Write-Error "Invalid ModulePath format: $ModulePath. Expected: Type.Name.Module or CommonModule.Name" + exit 1 +} + +$objType = $parts[0] +$objName = $parts[1] + +if (-not $typeDirMap.ContainsKey($objType)) { + Write-Error "Unknown object type: $objType" + exit 1 +} +$dirName = $typeDirMap[$objType] + +$bslFile = $null +if ($objType -eq "CommonModule") { + # CommonModule.X -> CommonModules/X/Ext/Module.bsl + $bslFile = Join-Path (Join-Path (Join-Path (Join-Path $ExtensionPath $dirName) $objName) "Ext") "Module.bsl" +} elseif ($parts.Count -ge 4 -and $parts[2] -eq "Form") { + # Type.X.Form.Y -> Types/X/Forms/Y/Ext/Form/Module.bsl + $formName = $parts[3] + $bslFile = Join-Path (Join-Path (Join-Path (Join-Path (Join-Path (Join-Path (Join-Path $ExtensionPath $dirName) $objName) "Forms") $formName) "Ext") "Form") "Module.bsl" +} elseif ($parts.Count -ge 3) { + # Type.X.ObjectModule -> Types/X/Ext/ObjectModule.bsl + $moduleName = $parts[2] + $moduleFileName = switch ($moduleName) { + "ObjectModule" { "ObjectModule.bsl" } + "ManagerModule" { "ManagerModule.bsl" } + "RecordSetModule" { "RecordSetModule.bsl" } + "CommandModule" { "CommandModule.bsl" } + default { "$moduleName.bsl" } + } + $bslFile = Join-Path (Join-Path (Join-Path $ExtensionPath $dirName) $objName) (Join-Path "Ext" $moduleFileName) +} else { + Write-Error "Invalid ModulePath format: $ModulePath. Expected: Type.Name.Module, Type.Name.Form.FormName, or CommonModule.Name" + exit 1 +} + +# --- Map InterceptorType to decorator --- +$decorator = switch ($InterceptorType) { + "Before" { "&Перед" } + "After" { "&После" } + "ModificationAndControl" { "&ИзменениеИКонтроль" } +} + +# --- Map Context to annotation --- +$contextAnnotation = switch ($Context) { + "НаСервере" { "&НаСервере" } + "НаКлиенте" { "&НаКлиенте" } + "НаСервереБезКонтекста" { "&НаСервереБезКонтекста" } + default { "&$Context" } +} + +# --- Procedure name --- +$procName = "${namePrefix}${MethodName}" + +# --- Generate BSL code --- +$keyword = if ($IsFunction) { "Функция" } else { "Процедура" } +$endKeyword = if ($IsFunction) { "КонецФункции" } else { "КонецПроцедуры" } + +$bodyLines = @() +switch ($InterceptorType) { + "Before" { + $bodyLines += "`t// TODO: код перед вызовом оригинального метода" + } + "After" { + $bodyLines += "`t// TODO: код после вызова оригинального метода" + } + "ModificationAndControl" { + $bodyLines += "`t// Скопируйте тело оригинального метода и внесите изменения," + $bodyLines += "`t// используя маркеры #Удаление / #КонецУдаления и #Вставка / #КонецВставки" + } +} + +if ($IsFunction) { + $bodyLines += "`t" + $bodyLines += "`tВозврат Неопределено; // TODO: заменить на реальное возвращаемое значение" +} + +$bslCode = @() +$bslCode += "$contextAnnotation" +$bslCode += "${decorator}(`"$MethodName`")" +$bslCode += "$keyword ${procName}()" +$bslCode += $bodyLines +$bslCode += "$endKeyword" + +$bslText = ($bslCode -join "`r`n") + "`r`n" + +# --- Check form borrowing for .Form. paths --- +if ($parts.Count -ge 4 -and $parts[2] -eq "Form") { + $formName = $parts[3] + $dirName = $typeDirMap[$objType] + $formMetaFile = Join-Path (Join-Path (Join-Path (Join-Path $ExtensionPath $dirName) $objName) "Forms") "${formName}.xml" + $formXmlFile = Join-Path (Join-Path (Join-Path (Join-Path (Join-Path $ExtensionPath $dirName) $objName) "Forms") $formName) "Ext/Form.xml" + + if (-not (Test-Path $formMetaFile) -or -not (Test-Path $formXmlFile)) { + Write-Host "[WARN] Form '$formName' metadata or Form.xml not found in extension." + Write-Host " Run /cfe-borrow first:" + Write-Host " /cfe-borrow -ExtensionPath $ExtensionPath -ConfigPath -Object `"$objType.$objName.Form.$formName`"" + Write-Host "" + } +} + +# --- Check if file exists and append --- +$bslDir = Split-Path $bslFile -Parent +if (-not (Test-Path $bslDir)) { + New-Item -ItemType Directory -Path $bslDir -Force | Out-Null +} + +$enc = New-Object System.Text.UTF8Encoding($true) + +if (Test-Path $bslFile) { + # Append to existing file + $existing = [System.IO.File]::ReadAllText($bslFile, $enc) + $separator = "`r`n" + if ($existing -and -not $existing.EndsWith("`n")) { + $separator = "`r`n`r`n" + } + $newContent = $existing + $separator + $bslText + [System.IO.File]::WriteAllText($bslFile, $newContent, $enc) + Write-Host "[OK] Добавлен перехватчик в существующий файл" +} else { + [System.IO.File]::WriteAllText($bslFile, $bslText, $enc) + Write-Host "[OK] Создан файл модуля" +} + +Write-Host " Файл: $bslFile" +Write-Host " Декоратор: $decorator(`"$MethodName`")" +Write-Host " Процедура: ${procName}()" +Write-Host " Контекст: $contextAnnotation" diff --git a/.codex/skills/cfe-patch-method/scripts/cfe-patch-method.py b/.codex/skills/cfe-patch-method/scripts/cfe-patch-method.py new file mode 100644 index 00000000..5a1ac770 --- /dev/null +++ b/.codex/skills/cfe-patch-method/scripts/cfe-patch-method.py @@ -0,0 +1,247 @@ +#!/usr/bin/env python3 +# cfe-patch-method v1.1 — 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(): + sys.stdout.reconfigure(encoding="utf-8") + sys.stderr.reconfigure(encoding="utf-8") + 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", + "Catalogs": "Catalogs", + "Documents": "Documents", + "Enums": "Enums", + "CommonModules": "CommonModules", + "Reports": "Reports", + "DataProcessors": "DataProcessors", + "ExchangePlans": "ExchangePlans", + "ChartsOfAccounts": "ChartsOfAccounts", + "ChartsOfCharacteristicTypes": "ChartsOfCharacteristicTypes", + "ChartsOfCalculationTypes": "ChartsOfCalculationTypes", + "BusinessProcesses": "BusinessProcesses", + "Tasks": "Tasks", + "InformationRegisters": "InformationRegisters", + "AccumulationRegisters": "AccumulationRegisters", + "AccountingRegisters": "AccountingRegisters", + "CalculationRegisters": "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", newline="") 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", newline="") 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", newline="") 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/.codex/skills/cfe-validate/SKILL.md b/.codex/skills/cfe-validate/SKILL.md new file mode 100644 index 00000000..d82b916f --- /dev/null +++ b/.codex/skills/cfe-validate/SKILL.md @@ -0,0 +1,29 @@ +--- +name: cfe-validate +description: Валидация расширения конфигурации 1С (CFE). Используй после создания или модификации расширения для проверки корректности +argument-hint: [-Detailed] [-MaxErrors 30] +allowed-tools: + - Bash + - Read + - Glob +--- + +# /cfe-validate — валидация расширения конфигурации (CFE) + +Проверяет структурную корректность расширения: XML-формат, свойства, состав, заимствованные объекты. Аналог `/cf-validate`, но для расширений. + +## Параметры + +| Параметр | Обяз. | Умолч. | Описание | +|---------------|:-----:|---------|-------------------------------------------------| +| ExtensionPath | да | — | Путь к каталогу или Configuration.xml расширения | +| Detailed | нет | — | Подробный вывод (все проверки, включая успешные) | +| MaxErrors | нет | 30 | Остановиться после N ошибок | +| OutFile | нет | — | Записать результат в файл | + +## Команда + +```powershell +powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/cfe-validate/scripts/cfe-validate.ps1" -ExtensionPath "src" +powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/cfe-validate/scripts/cfe-validate.ps1" -ExtensionPath "src/Configuration.xml" +``` diff --git a/.codex/skills/cfe-validate/scripts/cfe-validate.ps1 b/.codex/skills/cfe-validate/scripts/cfe-validate.ps1 new file mode 100644 index 00000000..2b6d5738 --- /dev/null +++ b/.codex/skills/cfe-validate/scripts/cfe-validate.ps1 @@ -0,0 +1,939 @@ +# cfe-validate v1.4 — Validate 1C configuration extension structure (CFE) +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( + [Parameter(Mandatory)] + [Alias('Path')] + [string]$ExtensionPath, + + [switch]$Detailed, + + [int]$MaxErrors = 30, + + [string]$OutFile +) + +$ErrorActionPreference = "Stop" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- Resolve path --- +if (-not [System.IO.Path]::IsPathRooted($ExtensionPath)) { + $ExtensionPath = Join-Path (Get-Location).Path $ExtensionPath +} + +if (Test-Path $ExtensionPath -PathType Container) { + $candidate = Join-Path $ExtensionPath "Configuration.xml" + if (Test-Path $candidate) { + $ExtensionPath = $candidate + } else { + Write-Host "[ERROR] No Configuration.xml found in directory: $ExtensionPath" + exit 1 + } +} + +if (-not (Test-Path $ExtensionPath)) { + Write-Host "[ERROR] File not found: $ExtensionPath" + exit 1 +} + +$resolvedPath = (Resolve-Path $ExtensionPath).Path +$configDir = Split-Path $resolvedPath -Parent + +# --- Output infrastructure --- +$script:errors = 0 +$script:warnings = 0 +$script:okCount = 0 +$script:stopped = $false +$script:output = New-Object System.Text.StringBuilder 8192 + +function Out-Line { + param([string]$msg) + $script:output.AppendLine($msg) | Out-Null +} + +function Report-OK { + param([string]$msg) + $script:okCount++ + if ($Detailed) { Out-Line "[OK] $msg" } +} + +function Report-Error { + param([string]$msg) + $script:errors++ + Out-Line "[ERROR] $msg" + if ($script:errors -ge $MaxErrors) { + $script:stopped = $true + } +} + +function Report-Warn { + param([string]$msg) + $script:warnings++ + Out-Line "[WARN] $msg" +} + +$finalize = { + $checks = $script:okCount + $script:errors + $script:warnings + if ($script:errors -eq 0 -and $script:warnings -eq 0 -and -not $Detailed) { + $result = "=== Validation OK: Extension.$objName ($checks checks) ===" + } else { + Out-Line "" + Out-Line "=== Result: $($script:errors) errors, $($script:warnings) warnings ($checks checks) ===" + $result = $script:output.ToString() + } + Write-Host $result + + if ($OutFile) { + $utf8Bom = New-Object System.Text.UTF8Encoding $true + [System.IO.File]::WriteAllText($OutFile, $result, $utf8Bom) + Write-Host "Written to: $OutFile" + } +} + +# --- Reference tables --- +$guidPattern = '^[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}$' +$identPattern = '^[A-Za-z\u0410-\u042F\u0401\u0430-\u044F\u0451_][A-Za-z0-9\u0410-\u042F\u0401\u0430-\u044F\u0451_]*$' + +# 7 fixed ClassIds for Configuration +$validClassIds = @( + "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 +$childObjectTypes = @( + "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 +$childTypeDirMap = @{ + "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 +$validEnumValues = @{ + "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","Version8_5_1") + "DefaultRunMode" = @("ManagedApplication","OrdinaryApplication","Auto") + "ScriptVariant" = @("Russian","English") + "InterfaceCompatibilityMode" = @("Version8_2","Version8_2EnableTaxi","Taxi","TaxiEnableVersion8_2","TaxiEnableVersion8_5","Version8_5EnableTaxi","Version8_5") +} + +# --- 1. Parse XML --- +Out-Line "" + +$xmlDoc = $null +try { + $xmlDoc = New-Object System.Xml.XmlDocument + $xmlDoc.PreserveWhitespace = $false + $xmlDoc.Load($resolvedPath) +} catch { + Out-Line "=== Validation: Extension (parse failed) ===" + Out-Line "" + Report-Error "1. XML parse failed: $($_.Exception.Message)" + & $finalize + exit 1 +} + +# --- Register namespaces --- +$ns = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable) +$ns.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") +$ns.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core") +$ns.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable") +$ns.AddNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance") +$ns.AddNamespace("xs", "http://www.w3.org/2001/XMLSchema") +$ns.AddNamespace("app", "http://v8.1c.ru/8.2/managed-application/core") + +$root = $xmlDoc.DocumentElement + +# --- Check 1: Root structure --- +$check1Ok = $true +$expectedNs = "http://v8.1c.ru/8.3/MDClasses" + +if ($root.LocalName -ne "MetaDataObject") { + Report-Error "1. Root element is '$($root.LocalName)', expected 'MetaDataObject'" + & $finalize + exit 1 +} + +if ($root.NamespaceURI -ne $expectedNs) { + Report-Error "1. Root namespace is '$($root.NamespaceURI)', expected '$expectedNs'" + $check1Ok = $false +} + +$version = $root.GetAttribute("version") +if (-not $version) { + Report-Warn "1. Missing version attribute on MetaDataObject" +} elseif ($version -ne "2.17" -and $version -ne "2.20" -and $version -ne "2.21") { + Report-Warn "1. Unusual version '$version' (expected 2.17, 2.20 or 2.21)" +} + +# Must have Configuration child +$cfgNode = $null +foreach ($child in $root.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Configuration" -and $child.NamespaceURI -eq $expectedNs) { + $cfgNode = $child; break + } +} + +if (-not $cfgNode) { + Report-Error "1. No element found inside MetaDataObject" + & $finalize + exit 1 +} + +# UUID +$cfgUuid = $cfgNode.GetAttribute("uuid") +if (-not $cfgUuid) { + Report-Error "1. Missing uuid on " + $check1Ok = $false +} elseif ($cfgUuid -notmatch $guidPattern) { + Report-Error "1. Invalid uuid '$cfgUuid' on " + $check1Ok = $false +} + +# Get name early for header +$propsNode = $cfgNode.SelectSingleNode("md:Properties", $ns) +$nameNode = if ($propsNode) { $propsNode.SelectSingleNode("md:Name", $ns) } else { $null } +$objName = if ($nameNode -and $nameNode.InnerText) { $nameNode.InnerText } else { "(unknown)" } + +$script:output.Insert(0, "=== Validation: Extension.$objName ===$([Environment]::NewLine)") | Out-Null + +if ($check1Ok) { + Report-OK "1. Root structure: MetaDataObject/Configuration, version $version" +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 2: InternalInfo --- +$internalInfo = $cfgNode.SelectSingleNode("md:InternalInfo", $ns) +$check2Ok = $true + +if (-not $internalInfo) { + Report-Error "2. InternalInfo: missing" +} else { + $contained = $internalInfo.SelectNodes("xr:ContainedObject", $ns) + if ($contained.Count -ne 7) { + Report-Warn "2. InternalInfo: expected 7 ContainedObject, found $($contained.Count)" + } + + $foundClassIds = @{} + foreach ($co in $contained) { + $classId = $co.SelectSingleNode("xr:ClassId", $ns) + $objectId = $co.SelectSingleNode("xr:ObjectId", $ns) + + if (-not $classId -or -not $classId.InnerText) { + Report-Error "2. ContainedObject missing ClassId" + $check2Ok = $false + continue + } + + $cid = $classId.InnerText + if ($validClassIds -notcontains $cid) { + Report-Error "2. Unknown ClassId: $cid" + $check2Ok = $false + } + + if ($foundClassIds.ContainsKey($cid)) { + Report-Error "2. Duplicate ClassId: $cid" + $check2Ok = $false + } + $foundClassIds[$cid] = $true + + if (-not $objectId -or -not $objectId.InnerText) { + Report-Error "2. ContainedObject missing ObjectId for ClassId $cid" + $check2Ok = $false + } elseif ($objectId.InnerText -notmatch $guidPattern) { + Report-Error "2. Invalid ObjectId '$($objectId.InnerText)' for ClassId $cid" + $check2Ok = $false + } + } + + $missingIds = @($validClassIds | Where-Object { -not $foundClassIds.ContainsKey($_) }) + if ($missingIds.Count -gt 0) { + Report-Warn "2. Missing ClassIds: $($missingIds.Count) of 7" + } + + if ($check2Ok) { + Report-OK "2. InternalInfo: $($contained.Count) ContainedObject, all ClassIds valid" + } +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 3: Extension-specific properties --- +if (-not $propsNode) { + Report-Error "3. Properties block missing" +} else { + $check3Ok = $true + + # ObjectBelonging = Adopted + $obNode = $propsNode.SelectSingleNode("md:ObjectBelonging", $ns) + if (-not $obNode -or $obNode.InnerText -ne "Adopted") { + Report-Error "3. ObjectBelonging must be 'Adopted', got '$($obNode.InnerText)'" + $check3Ok = $false + } + + # Name + if (-not $nameNode -or -not $nameNode.InnerText) { + Report-Error "3. Name is missing or empty" + $check3Ok = $false + } else { + $nameVal = $nameNode.InnerText + if ($nameVal -notmatch $identPattern) { + Report-Error "3. Name '$nameVal' is not a valid 1C identifier" + $check3Ok = $false + } + } + + # ConfigurationExtensionPurpose + $purposeNode = $propsNode.SelectSingleNode("md:ConfigurationExtensionPurpose", $ns) + $validPurposes = @("Patch","Customization","AddOn") + if (-not $purposeNode -or -not $purposeNode.InnerText) { + Report-Error "3. ConfigurationExtensionPurpose is missing" + $check3Ok = $false + } elseif ($validPurposes -notcontains $purposeNode.InnerText) { + Report-Error "3. ConfigurationExtensionPurpose '$($purposeNode.InnerText)' invalid (expected: Patch, Customization, AddOn)" + $check3Ok = $false + } + + # NamePrefix + $prefixNode = $propsNode.SelectSingleNode("md:NamePrefix", $ns) + if (-not $prefixNode -or -not $prefixNode.InnerText) { + Report-Warn "3. NamePrefix is empty" + } + + # KeepMappingToExtendedConfigurationObjectsByIDs + $keepMapNode = $propsNode.SelectSingleNode("md:KeepMappingToExtendedConfigurationObjectsByIDs", $ns) + if (-not $keepMapNode) { + Report-Warn "3. KeepMappingToExtendedConfigurationObjectsByIDs is missing" + } + + # DefaultLanguage + $defLangNode = $propsNode.SelectSingleNode("md:DefaultLanguage", $ns) + $defLang = if ($defLangNode -and $defLangNode.InnerText) { $defLangNode.InnerText } else { "" } + + if ($check3Ok) { + $purposeVal = if ($purposeNode) { $purposeNode.InnerText } else { "?" } + $prefixVal = if ($prefixNode -and $prefixNode.InnerText) { $prefixNode.InnerText } else { "(empty)" } + Report-OK "3. Extension properties: Name=`"$objName`", Purpose=$purposeVal, Prefix=$prefixVal" + } +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 4: Enum property values --- +if ($propsNode) { + $enumChecked = 0 + $check4Ok = $true + + foreach ($propName in $validEnumValues.Keys) { + $propNode = $propsNode.SelectSingleNode("md:$propName", $ns) + if ($propNode -and $propNode.InnerText) { + $val = $propNode.InnerText + $allowed = $validEnumValues[$propName] + if ($allowed -notcontains $val) { + Report-Error "4. Property '$propName' has invalid value '$val'" + $check4Ok = $false + } + $enumChecked++ + } + } + + if ($check4Ok) { + Report-OK "4. Property values: $enumChecked enum properties checked" + } +} else { + Report-Warn "4. No Properties block to check" +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 5: ChildObjects — valid types, no duplicates, order --- +$childObjNode = $cfgNode.SelectSingleNode("md:ChildObjects", $ns) + +if (-not $childObjNode) { + Report-Error "5. ChildObjects block missing" +} else { + $check5Ok = $true + $totalCount = 0 + $script:childObjectIndex = @{} + $duplicates = @{} + $typeFirstIndex = @{} + $lastTypeOrder = -1 + $orderOk = $true + + foreach ($child in $childObjNode.ChildNodes) { + if ($child.NodeType -ne 'Element') { continue } + $typeName = $child.LocalName + $objNameVal = $child.InnerText + + $typeIdx = $childObjectTypes.IndexOf($typeName) + if ($typeIdx -lt 0) { + Report-Error "5. Unknown type '$typeName' in ChildObjects" + $check5Ok = $false + } else { + if (-not $typeFirstIndex.ContainsKey($typeName)) { + $typeFirstIndex[$typeName] = $typeIdx + if ($typeIdx -lt $lastTypeOrder) { + Report-Warn "5. Type '$typeName' is out of canonical order (after type at position $lastTypeOrder)" + $orderOk = $false + } + $lastTypeOrder = $typeIdx + } + } + + if (-not $script:childObjectIndex.ContainsKey($typeName)) { $script:childObjectIndex[$typeName] = @{} } + if ($script:childObjectIndex[$typeName].ContainsKey($objNameVal)) { + if (-not $duplicates.ContainsKey("$typeName.$objNameVal")) { + Report-Error "5. Duplicate: $typeName.$objNameVal" + $duplicates["$typeName.$objNameVal"] = $true + $check5Ok = $false + } + } else { + $script:childObjectIndex[$typeName][$objNameVal] = $true + } + + $totalCount++ + } + + $typeCount = $script:childObjectIndex.Count + if ($check5Ok) { + $orderInfo = if ($orderOk) { ", order correct" } else { "" } + Report-OK "5. ChildObjects: $typeCount types, $totalCount objects${orderInfo}" + } +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 6: DefaultLanguage references existing Language in ChildObjects --- +if ($defLang -and $childObjNode) { + $langName = $defLang + if ($langName.StartsWith("Language.")) { + $langName = $langName.Substring(9) + } + + $found = $false + foreach ($child in $childObjNode.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Language" -and $child.InnerText -eq $langName) { + $found = $true; break + } + } + + if ($found) { + Report-OK "6. DefaultLanguage `"$defLang`" found in ChildObjects" + } else { + Report-Error "6. DefaultLanguage `"$defLang`" not found in ChildObjects" + } +} else { + if (-not $defLang) { + Report-Warn "6. Cannot check DefaultLanguage (empty)" + } else { + Report-Warn "6. Cannot check DefaultLanguage (no ChildObjects)" + } +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 7: Language files exist --- +if ($childObjNode) { + $langNames = @() + foreach ($child in $childObjNode.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.LocalName -eq "Language") { + $langNames += $child.InnerText + } + } + + if ($langNames.Count -gt 0) { + $existCount = 0 + foreach ($ln in $langNames) { + $langFile = Join-Path (Join-Path $configDir "Languages") "$ln.xml" + if (Test-Path $langFile) { + $existCount++ + } else { + Report-Warn "7. Language file missing: Languages/$ln.xml" + } + } + if ($existCount -eq $langNames.Count) { + Report-OK "7. Language files: $existCount/$($langNames.Count) exist" + } + } else { + Report-Warn "7. No Language entries in ChildObjects" + } +} else { + Report-Warn "7. Cannot check language files (no ChildObjects)" +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 8: Object directories exist --- +if ($childObjNode) { + $dirsToCheck = @{} + foreach ($child in $childObjNode.ChildNodes) { + if ($child.NodeType -ne 'Element') { continue } + $typeName = $child.LocalName + if ($typeName -eq "Language") { continue } + if ($childTypeDirMap.ContainsKey($typeName)) { + $dirName = $childTypeDirMap[$typeName] + if (-not $dirsToCheck.ContainsKey($dirName)) { + $dirsToCheck[$dirName] = 0 + } + $dirsToCheck[$dirName] = $dirsToCheck[$dirName] + 1 + } + } + + $missingDirs = @() + foreach ($dir in $dirsToCheck.Keys) { + $dirPath = Join-Path $configDir $dir + if (-not (Test-Path $dirPath -PathType Container)) { + $missingDirs += "$dir ($($dirsToCheck[$dir]) objects)" + } + } + + if ($missingDirs.Count -eq 0) { + Report-OK "8. Object directories: $($dirsToCheck.Count) directories, all exist" + } else { + foreach ($md in $missingDirs) { + Report-Warn "8. Missing directory: $md" + } + } +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 9: Borrowed objects validation + Check 10: Sub-items --- +$script:enumValuesIndex = @{} +$script:formList = @() + +# Helper: check if sub-item has explicit borrowed metadata +function Test-BorrowedSubItem { + param($subItem, $nsm) + $subProps = $subItem.SelectSingleNode("md:Properties", $nsm) + if (-not $subProps) { return $false } + $subOb = $subProps.SelectSingleNode("md:ObjectBelonging", $nsm) + if ($subOb -and $subOb.InnerText) { return $true } + $subExt = $subProps.SelectSingleNode("md:ExtendedConfigurationObject", $nsm) + return [bool]($subExt -and $subExt.InnerText) +} + +# Helper: validate a borrowed Attribute/EnumValue sub-item +function Validate-BorrowedSubItem { + param([string]$checkNum, [string]$context, [string]$subType, $subItem, $nsm) + $subProps = $subItem.SelectSingleNode("md:Properties", $nsm) + if (-not $subProps) { + Report-Error "${checkNum}. ${context}: ${subType} missing Properties" + return $false + } + $ok = $true + $subOb = $subProps.SelectSingleNode("md:ObjectBelonging", $nsm) + if (-not $subOb -or $subOb.InnerText -ne "Adopted") { + Report-Error "${checkNum}. ${context}: ${subType} ObjectBelonging must be 'Adopted'" + $ok = $false + } + $subName = $subProps.SelectSingleNode("md:Name", $nsm) + if (-not $subName -or -not $subName.InnerText) { + Report-Error "${checkNum}. ${context}: ${subType} missing Name" + $ok = $false + } + $subExt = $subProps.SelectSingleNode("md:ExtendedConfigurationObject", $nsm) + if (-not $subExt -or -not $subExt.InnerText) { + Report-Error "${checkNum}. ${context}: ${subType}.$($subName.InnerText) missing ExtendedConfigurationObject" + $ok = $false + } elseif ($subExt.InnerText -notmatch $guidPattern) { + Report-Error "${checkNum}. ${context}: ${subType}.$($subName.InnerText) invalid ExtendedConfigurationObject" + $ok = $false + } + return $ok +} + +if ($childObjNode) { + $borrowedCount = 0 + $borrowedOk = 0 + $check9Ok = $true + $check10Ok = $true + $subItemCount = 0 + + foreach ($child in $childObjNode.ChildNodes) { + if ($child.NodeType -ne 'Element') { continue } + $typeName = $child.LocalName + $childName = $child.InnerText + if ($typeName -eq "Language") { continue } + + if (-not $childTypeDirMap.ContainsKey($typeName)) { continue } + $dirName = $childTypeDirMap[$typeName] + $objFile = Join-Path (Join-Path $configDir $dirName) "$childName.xml" + + if (-not (Test-Path $objFile)) { continue } + + # Parse object XML + $objDoc = $null + try { + $objDoc = New-Object System.Xml.XmlDocument + $objDoc.PreserveWhitespace = $false + $objDoc.Load($objFile) + } catch { + Report-Warn "9. Cannot parse $dirName/$childName.xml: $($_.Exception.Message)" + continue + } + + $objNs = New-Object System.Xml.XmlNamespaceManager($objDoc.NameTable) + $objNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") + $objNs.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable") + + # Find the object element (Catalog, Document, etc.) + $objRoot = $objDoc.DocumentElement + $objEl = $null + foreach ($c in $objRoot.ChildNodes) { + if ($c.NodeType -eq 'Element') { $objEl = $c; break } + } + if (-not $objEl) { continue } + + $objProps = $objEl.SelectSingleNode("md:Properties", $objNs) + if (-not $objProps) { continue } + + # --- Check 9: ObjectBelonging + ExtendedConfigurationObject --- + $obNode = $objProps.SelectSingleNode("md:ObjectBelonging", $objNs) + if ($obNode -and $obNode.InnerText -eq "Adopted") { + $borrowedCount++ + + $extObj = $objProps.SelectSingleNode("md:ExtendedConfigurationObject", $objNs) + if (-not $extObj -or -not $extObj.InnerText) { + Report-Error "9. Borrowed ${typeName}.${childName}: missing ExtendedConfigurationObject" + $check9Ok = $false + } elseif ($extObj.InnerText -notmatch $guidPattern) { + Report-Error "9. Borrowed ${typeName}.${childName}: invalid ExtendedConfigurationObject UUID '$($extObj.InnerText)'" + $check9Ok = $false + } else { + $borrowedOk++ + } + } + + # --- Check 10: Sub-items (Attribute, TabularSection, EnumValue, Form) --- + $objChildObjects = $objEl.SelectSingleNode("md:ChildObjects", $objNs) + if ($objChildObjects) { + $ctx = "${typeName}.${childName}" + foreach ($subItem in $objChildObjects.ChildNodes) { + if ($subItem.NodeType -ne 'Element') { continue } + $subType = $subItem.LocalName + + if ($subType -eq "Attribute") { + if (-not (Test-BorrowedSubItem $subItem $objNs)) { continue } + $subItemCount++ + if (-not (Validate-BorrowedSubItem "10" $ctx "Attribute" $subItem $objNs)) { + $check10Ok = $false + } + } + elseif ($subType -eq "TabularSection") { + if (-not (Test-BorrowedSubItem $subItem $objNs)) { continue } + $subItemCount++ + if (-not (Validate-BorrowedSubItem "10" $ctx "TabularSection" $subItem $objNs)) { + $check10Ok = $false + } else { + # Check InternalInfo GeneratedTypes + $tsInfo = $subItem.SelectSingleNode("md:InternalInfo", $objNs) + $tsName = $subItem.SelectSingleNode("md:Properties/md:Name", $objNs) + $tsLabel = if ($tsName) { $tsName.InnerText } else { "?" } + if (-not $tsInfo) { + Report-Error "10. ${ctx}: TabularSection.${tsLabel} missing InternalInfo" + $check10Ok = $false + } else { + $gtNodes = $tsInfo.SelectNodes("xr:GeneratedType", $objNs) + $hasTSCat = $false; $hasTSRCat = $false + foreach ($gt in $gtNodes) { + $cat = $gt.GetAttribute("category") + if ($cat -eq "TabularSection") { $hasTSCat = $true } + if ($cat -eq "TabularSectionRow") { $hasTSRCat = $true } + } + if (-not $hasTSCat -or -not $hasTSRCat) { + Report-Error "10. ${ctx}: TabularSection.${tsLabel} missing GeneratedType (need TabularSection + TabularSectionRow)" + $check10Ok = $false + } + } + # Recurse into TS ChildObjects/Attribute + $tsChildObjs = $subItem.SelectSingleNode("md:ChildObjects", $objNs) + if ($tsChildObjs) { + foreach ($tsAttr in $tsChildObjs.ChildNodes) { + if ($tsAttr.NodeType -ne 'Element' -or $tsAttr.LocalName -ne "Attribute") { continue } + if (-not (Test-BorrowedSubItem $tsAttr $objNs)) { continue } + $subItemCount++ + if (-not (Validate-BorrowedSubItem "10" "${ctx}.ТЧ.${tsLabel}" "Attribute" $tsAttr $objNs)) { + $check10Ok = $false + } + } + } + } + } + elseif ($subType -eq "EnumValue" -and $typeName -eq "Enum") { + if (-not (Test-BorrowedSubItem $subItem $objNs)) { continue } + $subItemCount++ + if (Validate-BorrowedSubItem "10" $ctx "EnumValue" $subItem $objNs) { + $evName = $subItem.SelectSingleNode("md:Properties/md:Name", $objNs) + if ($evName -and $evName.InnerText) { + if (-not $script:enumValuesIndex.ContainsKey($childName)) { + $script:enumValuesIndex[$childName] = @{} + } + $script:enumValuesIndex[$childName][$evName.InnerText] = $true + } + } else { + $check10Ok = $false + } + } + elseif ($subType -eq "Form") { + $formName = $subItem.InnerText + if ($formName) { + $formMetaFile = Join-Path (Join-Path (Join-Path (Join-Path $configDir $dirName) $childName) "Forms") "${formName}.xml" + if (-not (Test-Path $formMetaFile)) { + Report-Error "10. ${ctx}: Form.${formName} metadata file missing" + $check10Ok = $false + } + $script:formList += @{ + TypeName = $typeName; ObjName = $childName + FormName = $formName; DirName = $dirName + } + $subItemCount++ + } + } + } + } + + if ($script:stopped) { break } + } + + if ($borrowedCount -eq 0) { + Report-OK "9. Borrowed objects: none found" + } elseif ($check9Ok) { + Report-OK "9. Borrowed objects: $borrowedOk/$borrowedCount validated" + } + + if ($subItemCount -eq 0) { + Report-OK "10. Sub-items: none found" + } elseif ($check10Ok) { + Report-OK "10. Sub-items: $subItemCount validated (Attributes, TabularSections, EnumValues, Forms)" + } +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 11: Borrowed form structure --- +$script:borrowedFormsWithTree = @() +$check11Ok = $true +$formCount = 0 + +foreach ($fi in $script:formList) { + $formCount++ + $formBase = Join-Path (Join-Path (Join-Path (Join-Path $configDir $fi.DirName) $fi.ObjName) "Forms") $fi.FormName + $formMetaFile = Join-Path (Split-Path $formBase -Parent) "$($fi.FormName).xml" + $formXmlFile = Join-Path (Join-Path $formBase "Ext") "Form.xml" + $moduleBslFile = Join-Path (Join-Path (Join-Path $formBase "Ext") "Form") "Module.bsl" + $ctx = "$($fi.TypeName).$($fi.ObjName).Form.$($fi.FormName)" + + # Validate form metadata XML + if (Test-Path $formMetaFile) { + try { + $fmDoc = New-Object System.Xml.XmlDocument + $fmDoc.PreserveWhitespace = $false + $fmDoc.Load($formMetaFile) + $fmNs = New-Object System.Xml.XmlNamespaceManager($fmDoc.NameTable) + $fmNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") + + $fmEl = $null + foreach ($c in $fmDoc.DocumentElement.ChildNodes) { + if ($c.NodeType -eq 'Element') { $fmEl = $c; break } + } + if ($fmEl) { + $fmProps = $fmEl.SelectSingleNode("md:Properties", $fmNs) + if ($fmProps) { + $fmOb = $fmProps.SelectSingleNode("md:ObjectBelonging", $fmNs) + $isBorrowed = $fmOb -and $fmOb.InnerText -eq "Adopted" + if ($isBorrowed) { + $fmExt = $fmProps.SelectSingleNode("md:ExtendedConfigurationObject", $fmNs) + if (-not $fmExt -or $fmExt.InnerText -notmatch $guidPattern) { + Report-Error "11. ${ctx}: invalid/missing ExtendedConfigurationObject" + $check11Ok = $false + } + } + $fmType = $fmProps.SelectSingleNode("md:FormType", $fmNs) + if ($fmType -and $fmType.InnerText -ne "Managed") { + Report-Error "11. ${ctx}: FormType must be 'Managed', got '$($fmType.InnerText)'" + $check11Ok = $false + } + } + } + } catch { + Report-Warn "11. ${ctx}: Cannot parse metadata: $($_.Exception.Message)" + } + } + + # Form.xml must exist + if (-not (Test-Path $formXmlFile)) { + Report-Error "11. ${ctx}: Ext/Form.xml missing" + $check11Ok = $false + continue + } + + # Module.bsl should exist + if (-not (Test-Path $moduleBslFile)) { + Report-Warn "11. ${ctx}: Ext/Form/Module.bsl missing" + } + + # Read Form.xml as raw text for BaseForm checks + $formRawText = [System.IO.File]::ReadAllText($formXmlFile, [System.Text.Encoding]::UTF8) + + if ($formRawText -match ']+version=') { + Report-Warn "11. ${ctx}: missing version attribute" + } + + $script:borrowedFormsWithTree += @{ + Path = $formXmlFile; RawText = $formRawText; Context = $ctx + } + } +} + +if ($formCount -eq 0) { + Report-OK "11. Borrowed forms: none found" +} elseif ($check11Ok) { + $bfCount = $script:borrowedFormsWithTree.Count + Report-OK "11. Borrowed forms: $formCount validated ($bfCount with BaseForm)" +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 12: Form dependency references --- +$platformStyleItems = @{ + "TableHeaderBackColor"=$true; "AccentColor"=$true; "NormalTextFont"=$true + "FormBackColor"=$true; "ToolTipBackColor"=$true; "BorderColor"=$true + "FieldBackColor"=$true; "FieldTextColor"=$true; "ButtonBackColor"=$true + "ButtonTextColor"=$true; "AlternateRowColor"=$true; "SpecialTextColor"=$true + "TextFont"=$true; "ImportantColor"=$true; "FormTextColor"=$true + "SmallTextFont"=$true; "ExtraLargeTextFont"=$true; "LargeTextFont"=$true + "NormalTextColor"=$true; "GroupHeaderBackColor"=$true; "GroupHeaderFont"=$true + "ErrorColor"=$true; "SuccessColor"=$true; "WarningColor"=$true +} +$check12Ok = $true +$depCheckCount = 0 + +foreach ($bf in $script:borrowedFormsWithTree) { + $raw = $bf.RawText + $ctx = $bf.Context + $missingItems = @() + + # CommonPicture references + $cpRefs = @{} + foreach ($m in [regex]::Matches($raw, 'CommonPicture\.(\w+)')) { + $cpRefs[$m.Groups[1].Value] = $true + } + $cpIndex = $script:childObjectIndex["CommonPicture"] + foreach ($cpName in $cpRefs.Keys) { + $depCheckCount++ + if (-not $cpIndex -or -not $cpIndex.ContainsKey($cpName)) { + $missingItems += "CommonPicture.${cpName}" + } + } + + # StyleItem references + $siRefs = @{} + foreach ($m in [regex]::Matches($raw, 'style:([A-Za-z\u0410-\u044F\u0401\u0451_][A-Za-z0-9\u0410-\u044F\u0401\u0451_]*)')) { + $siRefs[$m.Groups[1].Value] = $true + } + $siIndex = $script:childObjectIndex["StyleItem"] + foreach ($siName in $siRefs.Keys) { + $depCheckCount++ + if ($platformStyleItems.ContainsKey($siName)) { continue } + if (-not $siIndex -or -not $siIndex.ContainsKey($siName)) { + $missingItems += "StyleItem.${siName}" + } + } + + # Enum DesignTimeRef references + $enumRefs = @{} + foreach ($m in [regex]::Matches($raw, 'xr:DesignTimeRef">Enum\.(\w+)\.EnumValue\.(\w+)')) { + $eKey = "$($m.Groups[1].Value).$($m.Groups[2].Value)" + $enumRefs[$eKey] = @{ Enum = $m.Groups[1].Value; Value = $m.Groups[2].Value } + } + $eIndex = $script:childObjectIndex["Enum"] + foreach ($entry in $enumRefs.Values) { + $depCheckCount++ + if (-not $eIndex -or -not $eIndex.ContainsKey($entry.Enum)) { + $missingItems += "Enum.$($entry.Enum)" + } elseif (-not $script:enumValuesIndex.ContainsKey($entry.Enum) -or -not $script:enumValuesIndex[$entry.Enum].ContainsKey($entry.Value)) { + $missingItems += "Enum.$($entry.Enum).EnumValue.$($entry.Value)" + } + } + + foreach ($mi in $missingItems) { + Report-Warn "12. ${ctx}: references ${mi} not borrowed in extension" + $check12Ok = $false + } +} + +if ($script:borrowedFormsWithTree.Count -eq 0) { + Report-OK "12. Form dependencies: no borrowed forms with tree" +} elseif ($check12Ok) { + Report-OK "12. Form dependencies: $depCheckCount references checked" +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 13: TypeLink with human-readable paths --- +$check13Ok = $true +$typeLinkCount = 0 + +foreach ($bf in $script:borrowedFormsWithTree) { + $raw = $bf.RawText + $ctx = $bf.Context + $matches = [regex]::Matches($raw, '\s*Items\.[^<]*') + if ($matches.Count -gt 0) { + $typeLinkCount += $matches.Count + Report-Warn "13. ${ctx}: $($matches.Count) TypeLink(s) with human-readable Items.* DataPath (should be stripped)" + $check13Ok = $false + } +} + +if ($script:borrowedFormsWithTree.Count -eq 0) { + Report-OK "13. TypeLink: no borrowed forms with tree" +} elseif ($check13Ok) { + Report-OK "13. TypeLink: clean" +} + +# --- Final output --- +& $finalize + +if ($script:errors -gt 0) { + exit 1 +} +exit 0 diff --git a/.codex/skills/cfe-validate/scripts/cfe-validate.py b/.codex/skills/cfe-validate/scripts/cfe-validate.py new file mode 100644 index 00000000..870851e7 --- /dev/null +++ b/.codex/skills/cfe-validate/scripts/cfe-validate.py @@ -0,0 +1,894 @@ +#!/usr/bin/env python3 +# cfe-validate v1.4 — 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', 'Version8_5_1', + ], + 'DefaultRunMode': ['ManagedApplication', 'OrdinaryApplication', 'Auto'], + 'ScriptVariant': ['Russian', 'English'], + 'InterfaceCompatibilityMode': [ + 'Version8_2', 'Version8_2EnableTaxi', 'Taxi', 'TaxiEnableVersion8_2', + 'TaxiEnableVersion8_5', 'Version8_5EnableTaxi', 'Version8_5', + ], +} + +EXPECTED_NS = 'http://v8.1c.ru/8.3/MDClasses' + + +class Reporter: + def __init__(self, max_errors, detailed=False): + self.errors = 0 + self.warnings = 0 + self.ok_count = 0 + self.stopped = False + self.max_errors = max_errors + self.detailed = detailed + self.lines = [] + self.obj_name = '(unknown)' + + def out(self, msg=''): + self.lines.append(msg) + + def ok(self, msg): + self.ok_count += 1 + if self.detailed: + 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): + checks = self.ok_count + self.errors + self.warnings + if self.errors == 0 and self.warnings == 0 and not self.detailed: + result = f'=== Validation OK: Extension.{self.obj_name} ({checks} checks) ===' + else: + self.out('') + self.out(f'=== Result: {self.errors} errors, {self.warnings} warnings ({checks} checks) ===') + result = self.text() + + print(result, end='' if '\r\n' in result else '\n') + + 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(): + sys.stdout.reconfigure(encoding="utf-8") + sys.stderr.reconfigure(encoding="utf-8") + parser = argparse.ArgumentParser( + description='Validate 1C configuration extension XML structure (CFE)', allow_abbrev=False + ) + parser.add_argument('-ExtensionPath', '-Path', dest='ExtensionPath', required=True) + parser.add_argument('-Detailed', action='store_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, detailed=args.Detailed) + 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', '2.21'): + r.warn(f"1. Unusual version '{version}' (expected 2.17, 2.20 or 2.21)") + + # 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.obj_name = obj_name + + 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 + child_object_index = {} + 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 child_object_index: + child_object_index[type_name] = {} + if obj_name_val in child_object_index[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: + child_object_index[type_name][obj_name_val] = True + + total_count += 1 + + type_count = len(child_object_index) + 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: + pass # no ChildObjects + + if r.stopped: + r.finalize(out_file) + sys.exit(1) + + # --- Check 9: Borrowed objects + Check 10: Sub-items --- + MD = NS['md'] + XR = NS['xr'] + enum_values_index = {} + form_list = [] + + def is_borrowed_sub_item(sub_item): + """Check if sub-item has explicit borrowed metadata (ObjectBelonging or ExtendedConfigurationObject).""" + sub_props = sub_item.find(f'{{{MD}}}Properties') + if sub_props is None: + return False + sub_ob = sub_props.find(f'{{{MD}}}ObjectBelonging') + if sub_ob is not None and (sub_ob.text or ''): + return True + sub_ext = sub_props.find(f'{{{MD}}}ExtendedConfigurationObject') + return sub_ext is not None and bool(sub_ext.text or '') + + def validate_borrowed_sub_item(check_num, context, sub_type, sub_item): + """Validate a borrowed Attribute/EnumValue/TabularSection sub-item.""" + sub_props = sub_item.find(f'{{{MD}}}Properties') + if sub_props is None: + r.error(f'{check_num}. {context}: {sub_type} missing Properties') + return False + ok = True + sub_ob = sub_props.find(f'{{{MD}}}ObjectBelonging') + if sub_ob is None or (sub_ob.text or '') != 'Adopted': + r.error(f"{check_num}. {context}: {sub_type} ObjectBelonging must be 'Adopted'") + ok = False + sub_name = sub_props.find(f'{{{MD}}}Name') + if sub_name is None or not (sub_name.text or ''): + r.error(f'{check_num}. {context}: {sub_type} missing Name') + ok = False + sub_ext = sub_props.find(f'{{{MD}}}ExtendedConfigurationObject') + sub_name_val = (sub_name.text or '') if sub_name is not None else '?' + if sub_ext is None or not (sub_ext.text or ''): + r.error(f'{check_num}. {context}: {sub_type}.{sub_name_val} missing ExtendedConfigurationObject') + ok = False + elif not GUID_PATTERN.match(sub_ext.text): + r.error(f'{check_num}. {context}: {sub_type}.{sub_name_val} invalid ExtendedConfigurationObject') + ok = False + return ok + + if child_obj_node is not None: + borrowed_count = 0 + borrowed_ok_count = 0 + check9_ok = True + check10_ok = True + sub_item_count = 0 + + 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 + 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'{{{MD}}}Properties') + if obj_props is None: + continue + + # --- Check 9: ObjectBelonging + ExtendedConfigurationObject --- + ob_node = obj_props.find(f'{{{MD}}}ObjectBelonging') + if ob_node is not None and (ob_node.text or '') == 'Adopted': + borrowed_count += 1 + + ext_obj = obj_props.find(f'{{{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 + + # --- Check 10: Sub-items (Attribute, TabularSection, EnumValue, Form) --- + obj_child_objects = obj_el.find(f'{{{MD}}}ChildObjects') + if obj_child_objects is not None: + ctx = f'{type_name}.{child_name}' + for sub_item in obj_child_objects: + if not isinstance(sub_item.tag, str): + continue + sub_type = etree.QName(sub_item.tag).localname + + if sub_type == 'Attribute': + if not is_borrowed_sub_item(sub_item): + continue + sub_item_count += 1 + if not validate_borrowed_sub_item('10', ctx, 'Attribute', sub_item): + check10_ok = False + + elif sub_type == 'TabularSection': + if not is_borrowed_sub_item(sub_item): + continue + sub_item_count += 1 + if not validate_borrowed_sub_item('10', ctx, 'TabularSection', sub_item): + check10_ok = False + else: + # Check InternalInfo GeneratedTypes + ts_info = sub_item.find(f'{{{MD}}}InternalInfo') + ts_name_el = sub_item.find(f'{{{MD}}}Properties/{{{MD}}}Name') + ts_label = (ts_name_el.text or '?') if ts_name_el is not None else '?' + if ts_info is None: + r.error(f'10. {ctx}: TabularSection.{ts_label} missing InternalInfo') + check10_ok = False + else: + gt_nodes = ts_info.findall(f'{{{XR}}}GeneratedType') + has_ts = any(gt.get('category') == 'TabularSection' for gt in gt_nodes) + has_tsr = any(gt.get('category') == 'TabularSectionRow' for gt in gt_nodes) + if not has_ts or not has_tsr: + r.error(f'10. {ctx}: TabularSection.{ts_label} missing GeneratedType (need TabularSection + TabularSectionRow)') + check10_ok = False + # Recurse into TS ChildObjects/Attribute + ts_child_objs = sub_item.find(f'{{{MD}}}ChildObjects') + if ts_child_objs is not None: + for ts_attr in ts_child_objs: + if not isinstance(ts_attr.tag, str): + continue + if etree.QName(ts_attr.tag).localname != 'Attribute': + continue + if not is_borrowed_sub_item(ts_attr): + continue + sub_item_count += 1 + if not validate_borrowed_sub_item('10', f'{ctx}.ТЧ.{ts_label}', 'Attribute', ts_attr): + check10_ok = False + + elif sub_type == 'EnumValue' and type_name == 'Enum': + if not is_borrowed_sub_item(sub_item): + continue + sub_item_count += 1 + if validate_borrowed_sub_item('10', ctx, 'EnumValue', sub_item): + ev_name = sub_item.find(f'{{{MD}}}Properties/{{{MD}}}Name') + if ev_name is not None and (ev_name.text or ''): + if child_name not in enum_values_index: + enum_values_index[child_name] = {} + enum_values_index[child_name][ev_name.text] = True + else: + check10_ok = False + + elif sub_type == 'Form': + form_name = sub_item.text or '' + if form_name: + form_meta_file = os.path.join(config_dir, dir_name, child_name, 'Forms', form_name + '.xml') + if not os.path.exists(form_meta_file): + r.error(f'10. {ctx}: Form.{form_name} metadata file missing') + check10_ok = False + form_list.append({ + 'TypeName': type_name, 'ObjName': child_name, + 'FormName': form_name, 'DirName': dir_name, + }) + sub_item_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') + + if sub_item_count == 0: + r.ok('10. Sub-items: none found') + elif check10_ok: + r.ok(f'10. Sub-items: {sub_item_count} validated (Attributes, TabularSections, EnumValues, Forms)') + + if r.stopped: + r.finalize(out_file) + sys.exit(1) + + # --- Check 11: Borrowed form structure --- + borrowed_forms_with_tree = [] + check11_ok = True + form_count = 0 + + for fi in form_list: + form_count += 1 + form_base = os.path.join(config_dir, fi['DirName'], fi['ObjName'], 'Forms', fi['FormName']) + form_meta_file = os.path.join(os.path.dirname(form_base), fi['FormName'] + '.xml') + form_xml_file = os.path.join(form_base, 'Ext', 'Form.xml') + module_bsl_file = os.path.join(form_base, 'Ext', 'Form', 'Module.bsl') + ctx = f"{fi['TypeName']}.{fi['ObjName']}.Form.{fi['FormName']}" + + # Validate form metadata XML + if os.path.exists(form_meta_file): + try: + fm_doc = etree.parse(form_meta_file, etree.XMLParser(remove_blank_text=False)) + fm_root = fm_doc.getroot() + fm_el = None + for c in fm_root: + if isinstance(c.tag, str): + fm_el = c + break + if fm_el is not None: + fm_props = fm_el.find(f'{{{MD}}}Properties') + if fm_props is not None: + fm_ob = fm_props.find(f'{{{MD}}}ObjectBelonging') + is_borrowed = fm_ob is not None and (fm_ob.text or '') == 'Adopted' + if is_borrowed: + fm_ext = fm_props.find(f'{{{MD}}}ExtendedConfigurationObject') + if fm_ext is None or not (fm_ext.text or '') or not GUID_PATTERN.match(fm_ext.text or ''): + r.error(f'11. {ctx}: invalid/missing ExtendedConfigurationObject') + check11_ok = False + fm_type = fm_props.find(f'{{{MD}}}FormType') + if fm_type is not None and (fm_type.text or '') != 'Managed': + r.error(f"11. {ctx}: FormType must be 'Managed', got '{fm_type.text}'") + check11_ok = False + except etree.XMLSyntaxError as e: + r.warn(f'11. {ctx}: Cannot parse metadata: {e}') + + # Form.xml must exist + if not os.path.exists(form_xml_file): + r.error(f'11. {ctx}: Ext/Form.xml missing') + check11_ok = False + continue + + # Module.bsl should exist + if not os.path.exists(module_bsl_file): + r.warn(f'11. {ctx}: Ext/Form/Module.bsl missing') + + # Read Form.xml as raw text for BaseForm checks + with open(form_xml_file, 'r', encoding='utf-8-sig') as f: + form_raw_text = f.read() + + if ']+version=', form_raw_text): + r.warn(f'11. {ctx}: missing version attribute') + borrowed_forms_with_tree.append({ + 'Path': form_xml_file, 'RawText': form_raw_text, 'Context': ctx, + }) + + if form_count == 0: + r.ok('11. Borrowed forms: none found') + elif check11_ok: + bf_count = len(borrowed_forms_with_tree) + r.ok(f'11. Borrowed forms: {form_count} validated ({bf_count} with BaseForm)') + + if r.stopped: + r.finalize(out_file) + sys.exit(1) + + # --- Check 12: Form dependency references --- + PLATFORM_STYLE_ITEMS = { + 'TableHeaderBackColor', 'AccentColor', 'NormalTextFont', + 'FormBackColor', 'ToolTipBackColor', 'BorderColor', + 'FieldBackColor', 'FieldTextColor', 'ButtonBackColor', + 'ButtonTextColor', 'AlternateRowColor', 'SpecialTextColor', + 'TextFont', 'ImportantColor', 'FormTextColor', + 'SmallTextFont', 'ExtraLargeTextFont', 'LargeTextFont', + 'NormalTextColor', 'GroupHeaderBackColor', 'GroupHeaderFont', + 'ErrorColor', 'SuccessColor', 'WarningColor', + } + check12_ok = True + dep_check_count = 0 + + for bf in borrowed_forms_with_tree: + raw = bf['RawText'] + ctx = bf['Context'] + missing_items = [] + + # CommonPicture references + cp_refs = {} + for m in re.finditer(r'CommonPicture\.(\w+)', raw): + cp_refs[m.group(1)] = True + cp_index = child_object_index.get('CommonPicture', {}) + for cp_name in cp_refs: + dep_check_count += 1 + if cp_name not in cp_index: + missing_items.append(f'CommonPicture.{cp_name}') + + # StyleItem references + si_refs = {} + for m in re.finditer(r'style:([A-Za-z\u0410-\u044F\u0401\u0451_][A-Za-z0-9\u0410-\u044F\u0401\u0451_]*)', raw): + si_refs[m.group(1)] = True + si_index = child_object_index.get('StyleItem', {}) + for si_name in si_refs: + dep_check_count += 1 + if si_name in PLATFORM_STYLE_ITEMS: + continue + if si_name not in si_index: + missing_items.append(f'StyleItem.{si_name}') + + # Enum DesignTimeRef references + enum_refs = {} + for m in re.finditer(r'xr:DesignTimeRef">Enum\.(\w+)\.EnumValue\.(\w+)', raw): + e_key = f'{m.group(1)}.{m.group(2)}' + enum_refs[e_key] = {'Enum': m.group(1), 'Value': m.group(2)} + e_index = child_object_index.get('Enum', {}) + for entry in enum_refs.values(): + dep_check_count += 1 + if entry['Enum'] not in e_index: + missing_items.append(f"Enum.{entry['Enum']}") + elif entry['Enum'] not in enum_values_index or entry['Value'] not in enum_values_index.get(entry['Enum'], {}): + missing_items.append(f"Enum.{entry['Enum']}.EnumValue.{entry['Value']}") + + for mi in missing_items: + r.warn(f'12. {ctx}: references {mi} not borrowed in extension') + check12_ok = False + + if len(borrowed_forms_with_tree) == 0: + r.ok('12. Form dependencies: no borrowed forms with tree') + elif check12_ok: + r.ok(f'12. Form dependencies: {dep_check_count} references checked') + + if r.stopped: + r.finalize(out_file) + sys.exit(1) + + # --- Check 13: TypeLink with human-readable paths --- + check13_ok = True + type_link_count = 0 + + for bf in borrowed_forms_with_tree: + raw = bf['RawText'] + ctx = bf['Context'] + matches = re.findall(r'\s*Items\.[^<]*', raw) + if matches: + type_link_count += len(matches) + r.warn(f'13. {ctx}: {len(matches)} TypeLink(s) with human-readable Items.* DataPath (should be stripped)') + check13_ok = False + + if len(borrowed_forms_with_tree) == 0: + r.ok('13. TypeLink: no borrowed forms with tree') + elif check13_ok: + r.ok('13. TypeLink: clean') + + # --- Final output --- + r.finalize(out_file) + sys.exit(1 if r.errors > 0 else 0) + + +if __name__ == '__main__': + main() diff --git a/.codex/skills/db-create/SKILL.md b/.codex/skills/db-create/SKILL.md new file mode 100644 index 00000000..7a157f2d --- /dev/null +++ b/.codex/skills/db-create/SKILL.md @@ -0,0 +1,78 @@ +--- +name: db-create +description: Создание информационной базы 1С. Используй когда нужно создать базу, новую ИБ, пустую базу +argument-hint: +allowed-tools: + - Bash + - Read + - Write + - Glob + - AskUserQuestion +--- + +# /db-create — Создание информационной базы + +Создаёт новую информационную базу 1С (файловую или серверную) и предлагает зарегистрировать в `.v8-project.json`. + +## Usage + +``` +/db-create — файловая база по указанному пути +/db-create / — серверная база +/db-create — интерактивно +``` + +## Параметры подключения + +Прочитай `.v8-project.json` из корня проекта для `v8path` (путь к платформе). +Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1` +После создания базы предложи зарегистрировать через `/db-list add`. + +## Команда + +```powershell +powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/db-create/scripts/db-create.ps1" <параметры> +``` + +### Параметры скрипта + +| Параметр | Обязательный | Описание | +|----------|:------------:|----------| +| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) | +| `-InfoBasePath <путь>` | * | Путь к файловой базе | +| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) | +| `-InfoBaseRef <имя>` | * | Имя базы на сервере | +| `-UseTemplate <файл>` | нет | Создать из шаблона (.cf или .dt) | +| `-AddToList` | нет | Добавить в список баз 1С | +| `-ListName <имя>` | нет | Имя базы в списке | + +> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef` + +## Коды возврата + +| Код | Описание | +|-----|----------| +| 0 | Успешно | +| 1 | Ошибка (см. лог) | + +## После создания + +1. Прочитай лог-файл и покажи результат +2. Предложи зарегистрировать базу в `.v8-project.json` (через `/db-list add`) +3. Если указан шаблон `/UseTemplate` — предупреди что конфигурация будет загружена из шаблона + +## Примеры + +```powershell +# Создать файловую базу +powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/db-create/scripts/db-create.ps1" -InfoBasePath "C:\Bases\NewDB" + +# Создать серверную базу +powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/db-create/scripts/db-create.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test" + +# Создать из шаблона CF +powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/db-create/scripts/db-create.ps1" -InfoBasePath "C:\Bases\NewDB" -UseTemplate "C:\Templates\config.cf" + +# Создать и добавить в список баз +powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/db-create/scripts/db-create.ps1" -InfoBasePath "C:\Bases\NewDB" -AddToList -ListName "Новая база" +``` diff --git a/.codex/skills/db-create/scripts/db-create.ps1 b/.codex/skills/db-create/scripts/db-create.ps1 new file mode 100644 index 00000000..71d9ea9b --- /dev/null +++ b/.codex/skills/db-create/scripts/db-create.ps1 @@ -0,0 +1,163 @@ +# db-create v1.0 — Create 1C information base +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +<# +.SYNOPSIS + Создание информационной базы 1С + +.DESCRIPTION + Создаёт новую информационную базу 1С (файловую или серверную). + Поддерживает создание из шаблона и добавление в список баз. + +.PARAMETER V8Path + Путь к каталогу bin платформы или к 1cv8.exe + +.PARAMETER InfoBasePath + Путь к файловой информационной базе + +.PARAMETER InfoBaseServer + Сервер 1С (для серверной базы) + +.PARAMETER InfoBaseRef + Имя базы на сервере + +.PARAMETER UseTemplate + Путь к файлу шаблона (.cf или .dt) + +.PARAMETER AddToList + Добавить в список баз 1С + +.PARAMETER ListName + Имя базы в списке + +.EXAMPLE + .\db-create.ps1 -InfoBasePath "C:\Bases\NewDB" + +.EXAMPLE + .\db-create.ps1 -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test" + +.EXAMPLE + .\db-create.ps1 -InfoBasePath "C:\Bases\NewDB" -UseTemplate "C:\Templates\config.cf" -AddToList -ListName "Новая база" +#> + +[CmdletBinding()] +param( + [Parameter(Mandatory=$false)] + [string]$V8Path, + + [Parameter(Mandatory=$false)] + [string]$InfoBasePath, + + [Parameter(Mandatory=$false)] + [string]$InfoBaseServer, + + [Parameter(Mandatory=$false)] + [string]$InfoBaseRef, + + [Parameter(Mandatory=$false)] + [string]$UseTemplate, + + [Parameter(Mandatory=$false)] + [switch]$AddToList, + + [Parameter(Mandatory=$false)] + [string]$ListName +) + +$OutputEncoding = [System.Text.Encoding]::UTF8 +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- Resolve V8Path --- +if (-not $V8Path) { + $found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1 + if ($found) { + $V8Path = $found.FullName + } else { + Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red + exit 1 + } +} elseif (Test-Path $V8Path -PathType Container) { + $V8Path = Join-Path $V8Path "1cv8.exe" +} + +if (-not (Test-Path $V8Path)) { + Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red + exit 1 +} + +# --- Validate connection --- +if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) { + Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red + exit 1 +} + +# --- Validate template --- +if ($UseTemplate -and -not (Test-Path $UseTemplate)) { + Write-Host "Error: template file not found: $UseTemplate" -ForegroundColor Red + exit 1 +} + +# --- Temp dir --- +$tempDir = Join-Path $env:TEMP "db_create_$(Get-Random)" +New-Item -ItemType Directory -Path $tempDir -Force | Out-Null + +try { + # --- Build arguments --- + $arguments = @("CREATEINFOBASE") + + if ($InfoBaseServer -and $InfoBaseRef) { + $arguments += "Srvr=`"$InfoBaseServer`";Ref=`"$InfoBaseRef`"" + } else { + $arguments += "File=`"$InfoBasePath`"" + } + + # --- Template --- + if ($UseTemplate) { + $arguments += "/UseTemplate", "`"$UseTemplate`"" + } + + # --- Add to list --- + if ($AddToList) { + if ($ListName) { + $arguments += "/AddToList", "`"$ListName`"" + } else { + $arguments += "/AddToList" + } + } + + # --- Output --- + $outFile = Join-Path $tempDir "create_log.txt" + $arguments += "/Out", "`"$outFile`"" + $arguments += "/DisableStartupDialogs" + + # --- Execute --- + Write-Host "Running: 1cv8.exe $($arguments -join ' ')" + $process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru + $exitCode = $process.ExitCode + + # --- Result --- + if ($exitCode -eq 0) { + if ($InfoBaseServer -and $InfoBaseRef) { + Write-Host "Information base created successfully: $InfoBaseServer/$InfoBaseRef" -ForegroundColor Green + } else { + Write-Host "Information base created successfully: $InfoBasePath" -ForegroundColor Green + } + } else { + Write-Host "Error creating information base (code: $exitCode)" -ForegroundColor Red + } + + if (Test-Path $outFile) { + $logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue + if ($logContent) { + Write-Host "--- Log ---" + Write-Host $logContent + Write-Host "--- End ---" + } + } + + exit $exitCode + +} finally { + if (Test-Path $tempDir) { + Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue + } +} diff --git a/.codex/skills/db-create/scripts/db-create.py b/.codex/skills/db-create/scripts/db-create.py new file mode 100644 index 00000000..34f63ed8 --- /dev/null +++ b/.codex/skills/db-create/scripts/db-create.py @@ -0,0 +1,127 @@ +#!/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(): + sys.stdout.reconfigure(encoding="utf-8") + sys.stderr.reconfigure(encoding="utf-8") + 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", args.UseTemplate]) + + # --- Add to list --- + if args.AddToList: + if args.ListName: + arguments.extend(["/AddToList", args.ListName]) + else: + arguments.append("/AddToList") + + # --- Output --- + out_file = os.path.join(temp_dir, "create_log.txt") + arguments.extend(["/Out", 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/.codex/skills/db-dump-cf/SKILL.md b/.codex/skills/db-dump-cf/SKILL.md new file mode 100644 index 00000000..75f2473f --- /dev/null +++ b/.codex/skills/db-dump-cf/SKILL.md @@ -0,0 +1,79 @@ +--- +name: db-dump-cf +description: Выгрузка конфигурации 1С в CF-файл. Используй когда нужно выгрузить конфигурацию в CF, сохранить конфигурацию, сделать бэкап CF +argument-hint: "[database] [output.cf]" +allowed-tools: + - Bash + - Read + - Glob + - AskUserQuestion +--- + +# /db-dump-cf — Выгрузка конфигурации в CF-файл + +Выгружает конфигурацию информационной базы в бинарный CF-файл. + +## Usage + +``` +/db-dump-cf [database] [output.cf] +/db-dump-cf dev config.cf +/db-dump-cf — база по умолчанию, файл config.cf +``` + +## Параметры подключения + +Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу: +1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую +2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` +3. Если не указал — сопоставь текущую ветку Git с `databases[].branches` +4. Если ветка не совпала — используй `default` +Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1` +Если файла нет — предложи `/db-list add`. +Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. + +## Команда + +```powershell +powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/db-dump-cf/scripts/db-dump-cf.ps1" <параметры> +``` + +### Параметры скрипта + +| Параметр | Обязательный | Описание | +|----------|:------------:|----------| +| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) | +| `-InfoBasePath <путь>` | * | Файловая база | +| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) | +| `-InfoBaseRef <имя>` | * | Имя базы на сервере | +| `-UserName <имя>` | нет | Имя пользователя | +| `-Password <пароль>` | нет | Пароль | +| `-OutputFile <путь>` | да | Путь к выходному CF-файлу | +| `-Extension <имя>` | нет | Выгрузить расширение | +| `-AllExtensions` | нет | Выгрузить все расширения | + +> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef` + +## Коды возврата + +| Код | Описание | +|-----|----------| +| 0 | Успешно | +| 1 | Ошибка (см. лог) | + +## После выполнения + +Прочитай лог-файл и покажи результат. Если есть ошибки — покажи содержимое лога. + +## Примеры + +```powershell +# Выгрузка конфигурации (файловая база) +powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/db-dump-cf/scripts/db-dump-cf.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "C:\backup\config.cf" + +# Серверная база +powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/db-dump-cf/scripts/db-dump-cf.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -OutputFile "config.cf" + +# Выгрузка расширения +powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/db-dump-cf/scripts/db-dump-cf.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "ext.cfe" -Extension "МоёРасширение" +``` diff --git a/.codex/skills/db-dump-cf/scripts/db-dump-cf.ps1 b/.codex/skills/db-dump-cf/scripts/db-dump-cf.ps1 new file mode 100644 index 00000000..45f3090c --- /dev/null +++ b/.codex/skills/db-dump-cf/scripts/db-dump-cf.ps1 @@ -0,0 +1,166 @@ +# db-dump-cf v1.0 — Dump 1C configuration to CF file +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +<# +.SYNOPSIS + Выгрузка конфигурации 1С в CF-файл + +.DESCRIPTION + Выгружает конфигурацию информационной базы в бинарный CF-файл. + Поддерживает выгрузку расширений. + +.PARAMETER V8Path + Путь к каталогу bin платформы или к 1cv8.exe + +.PARAMETER InfoBasePath + Путь к файловой информационной базе + +.PARAMETER InfoBaseServer + Сервер 1С (для серверной базы) + +.PARAMETER InfoBaseRef + Имя базы на сервере + +.PARAMETER UserName + Имя пользователя 1С + +.PARAMETER Password + Пароль пользователя + +.PARAMETER OutputFile + Путь к выходному CF-файлу + +.PARAMETER Extension + Имя расширения для выгрузки + +.PARAMETER AllExtensions + Выгрузить все расширения + +.EXAMPLE + .\db-dump-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -OutputFile "config.cf" + +.EXAMPLE + .\db-dump-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -OutputFile "ext.cfe" -Extension "МоёРасширение" +#> + +[CmdletBinding()] +param( + [Parameter(Mandatory=$false)] + [string]$V8Path, + + [Parameter(Mandatory=$false)] + [string]$InfoBasePath, + + [Parameter(Mandatory=$false)] + [string]$InfoBaseServer, + + [Parameter(Mandatory=$false)] + [string]$InfoBaseRef, + + [Parameter(Mandatory=$false)] + [string]$UserName, + + [Parameter(Mandatory=$false)] + [string]$Password, + + [Parameter(Mandatory=$true)] + [string]$OutputFile, + + [Parameter(Mandatory=$false)] + [string]$Extension, + + [Parameter(Mandatory=$false)] + [switch]$AllExtensions +) + +$OutputEncoding = [System.Text.Encoding]::UTF8 +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- Resolve V8Path --- +if (-not $V8Path) { + $found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1 + if ($found) { + $V8Path = $found.FullName + } else { + Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red + exit 1 + } +} elseif (Test-Path $V8Path -PathType Container) { + $V8Path = Join-Path $V8Path "1cv8.exe" +} + +if (-not (Test-Path $V8Path)) { + Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red + exit 1 +} + +# --- Validate connection --- +if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) { + Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red + exit 1 +} + +# --- Ensure output directory exists --- +$outDir = Split-Path $OutputFile -Parent +if ($outDir -and -not (Test-Path $outDir)) { + New-Item -ItemType Directory -Path $outDir -Force | Out-Null +} + +# --- Temp dir --- +$tempDir = Join-Path $env:TEMP "db_dump_cf_$(Get-Random)" +New-Item -ItemType Directory -Path $tempDir -Force | Out-Null + +try { + # --- Build arguments --- + $arguments = @("DESIGNER") + + if ($InfoBaseServer -and $InfoBaseRef) { + $arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`"" + } else { + $arguments += "/F", "`"$InfoBasePath`"" + } + + if ($UserName) { $arguments += "/N`"$UserName`"" } + if ($Password) { $arguments += "/P`"$Password`"" } + + $arguments += "/DumpCfg", "`"$OutputFile`"" + + # --- Extensions --- + if ($Extension) { + $arguments += "-Extension", "`"$Extension`"" + } elseif ($AllExtensions) { + $arguments += "-AllExtensions" + } + + # --- Output --- + $outFile = Join-Path $tempDir "dump_cf_log.txt" + $arguments += "/Out", "`"$outFile`"" + $arguments += "/DisableStartupDialogs" + + # --- Execute --- + Write-Host "Running: 1cv8.exe $($arguments -join ' ')" + $process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru + $exitCode = $process.ExitCode + + # --- Result --- + if ($exitCode -eq 0) { + Write-Host "Configuration dumped successfully to: $OutputFile" -ForegroundColor Green + } else { + Write-Host "Error dumping configuration (code: $exitCode)" -ForegroundColor Red + } + + if (Test-Path $outFile) { + $logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue + if ($logContent) { + Write-Host "--- Log ---" + Write-Host $logContent + Write-Host "--- End ---" + } + } + + exit $exitCode + +} finally { + if (Test-Path $tempDir) { + Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue + } +} diff --git a/.codex/skills/db-dump-cf/scripts/db-dump-cf.py b/.codex/skills/db-dump-cf/scripts/db-dump-cf.py new file mode 100644 index 00000000..4471a751 --- /dev/null +++ b/.codex/skills/db-dump-cf/scripts/db-dump-cf.py @@ -0,0 +1,128 @@ +#!/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(): + sys.stdout.reconfigure(encoding="utf-8") + sys.stderr.reconfigure(encoding="utf-8") + 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", args.InfoBasePath]) + + if args.UserName: + arguments.append(f"/N{args.UserName}") + if args.Password: + arguments.append(f"/P{args.Password}") + + arguments.extend(["/DumpCfg", args.OutputFile]) + + # --- Extensions --- + if args.Extension: + arguments.extend(["-Extension", args.Extension]) + elif args.AllExtensions: + arguments.append("-AllExtensions") + + # --- Output --- + out_file = os.path.join(temp_dir, "dump_cf_log.txt") + arguments.extend(["/Out", 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/.codex/skills/db-dump-xml/SKILL.md b/.codex/skills/db-dump-xml/SKILL.md new file mode 100644 index 00000000..0d75744c --- /dev/null +++ b/.codex/skills/db-dump-xml/SKILL.md @@ -0,0 +1,97 @@ +--- +name: db-dump-xml +description: Выгрузка конфигурации 1С в XML-файлы. Используй когда нужно выгрузить конфигурацию в файлы, XML, исходники, DumpConfigToFiles +argument-hint: "[database] [outputDir]" +allowed-tools: + - Bash + - Read + - Glob + - AskUserQuestion +--- + +# /db-dump-xml — Выгрузка конфигурации в XML + +Выгружает конфигурацию информационной базы в XML-файлы (исходники). Поддерживает полную, инкрементальную, частичную выгрузку и обновление ConfigDumpInfo. + +## Usage + +``` +/db-dump-xml [database] [outputDir] +/db-dump-xml dev src/config +/db-dump-xml dev src/config -Mode Full +/db-dump-xml dev src/config -Mode Partial -Objects "Справочник.Номенклатура,Документ.Заказ" +``` + +## Параметры подключения + +Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу: +1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую +2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` +3. Если не указал — сопоставь текущую ветку Git с `databases[].branches` +4. Если ветка не совпала — используй `default` +Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1` +Если файла нет — предложи `/db-list add`. +Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. +Если в записи базы указан `configSrc` — используй как каталог выгрузки по умолчанию. + +## Команда + +```powershell +powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/db-dump-xml/scripts/db-dump-xml.ps1" <параметры> +``` + +### Параметры скрипта + +| Параметр | Обязательный | Описание | +|----------|:------------:|----------| +| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) | +| `-InfoBasePath <путь>` | * | Файловая база | +| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) | +| `-InfoBaseRef <имя>` | * | Имя базы на сервере | +| `-UserName <имя>` | нет | Имя пользователя | +| `-Password <пароль>` | нет | Пароль | +| `-ConfigDir <путь>` | да | Каталог для выгрузки | +| `-Mode <режим>` | нет | `Full` / `Changes` (по умолч.) / `Partial` / `UpdateInfo` | +| `-Objects <список>` | для Partial | Имена объектов через запятую | +| `-Extension <имя>` | нет | Выгрузить расширение | +| `-AllExtensions` | нет | Выгрузить все расширения | +| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` | + +> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef` + +### Режимы выгрузки + +| Режим | Описание | +|-------|----------| +| `Full` | Полная выгрузка — все объекты конфигурации | +| `Changes` | Инкрементальная — только изменённые с последней выгрузки (использует ConfigDumpInfo.xml) | +| `Partial` | Частичная — выбранные объекты из параметра `-Objects` | +| `UpdateInfo` | Обновить только ConfigDumpInfo.xml без выгрузки файлов | + +## Коды возврата + +| Код | Описание | +|-----|----------| +| 0 | Успешно | +| 1 | Ошибка (см. лог) | + +> Если пользователь просит выгрузить конкретные объекты — используй `-Mode Partial` с `-Objects`. + +## Примеры + +```powershell +# Полная выгрузка (файловая база) +powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/db-dump-xml/scripts/db-dump-xml.ps1" -V8Path "C:\Program Files\1cv8\8.3.25.1257\bin" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full + +# Инкрементальная выгрузка +powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/db-dump-xml/scripts/db-dump-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Changes + +# Частичная выгрузка +powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/db-dump-xml/scripts/db-dump-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Partial -Objects "Справочник.Номенклатура,Документ.Заказ" + +# Серверная база +powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/db-dump-xml/scripts/db-dump-xml.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -ConfigDir "C:\WS\cfsrc" -Mode Full + +# Выгрузка расширения +powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/db-dump-xml/scripts/db-dump-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\ext_src" -Mode Full -Extension "МоёРасширение" +``` diff --git a/.codex/skills/db-dump-xml/scripts/db-dump-xml.ps1 b/.codex/skills/db-dump-xml/scripts/db-dump-xml.ps1 new file mode 100644 index 00000000..8bc46360 --- /dev/null +++ b/.codex/skills/db-dump-xml/scripts/db-dump-xml.ps1 @@ -0,0 +1,224 @@ +# db-dump-xml v1.0 — Dump 1C configuration to XML files +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +<# +.SYNOPSIS + Выгрузка конфигурации 1С в XML-файлы + +.DESCRIPTION + Выполняет выгрузку конфигурации 1С в файлы в четырёх режимах: + - Full: полная выгрузка всей конфигурации + - Changes: инкрементальная выгрузка изменённых объектов + - Partial: выгрузка конкретных объектов из списка + - UpdateInfo: обновление только ConfigDumpInfo.xml + +.PARAMETER V8Path + Путь к каталогу bin платформы или к 1cv8.exe + +.PARAMETER InfoBasePath + Путь к файловой информационной базе + +.PARAMETER InfoBaseServer + Сервер 1С (для серверной базы) + +.PARAMETER InfoBaseRef + Имя базы на сервере + +.PARAMETER UserName + Имя пользователя 1С + +.PARAMETER Password + Пароль пользователя + +.PARAMETER ConfigDir + Каталог для выгрузки конфигурации + +.PARAMETER Mode + Режим выгрузки: Full, Changes, Partial, UpdateInfo (по умолчанию Changes) + +.PARAMETER Objects + Имена объектов метаданных через запятую (для режима Partial) + +.PARAMETER Extension + Имя расширения для выгрузки + +.PARAMETER AllExtensions + Выгрузить все расширения + +.PARAMETER Format + Формат выгрузки: Hierarchical или Plain (по умолчанию Hierarchical) + +.EXAMPLE + .\db-dump-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Full + +.EXAMPLE + .\db-dump-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Partial -Objects "Справочник.Номенклатура,Документ.Заказ" +#> + +[CmdletBinding()] +param( + [Parameter(Mandatory=$false)] + [string]$V8Path, + + [Parameter(Mandatory=$false)] + [string]$InfoBasePath, + + [Parameter(Mandatory=$false)] + [string]$InfoBaseServer, + + [Parameter(Mandatory=$false)] + [string]$InfoBaseRef, + + [Parameter(Mandatory=$false)] + [string]$UserName, + + [Parameter(Mandatory=$false)] + [string]$Password, + + [Parameter(Mandatory=$true)] + [string]$ConfigDir, + + [Parameter(Mandatory=$false)] + [ValidateSet("Full", "Changes", "Partial", "UpdateInfo")] + [string]$Mode = "Changes", + + [Parameter(Mandatory=$false)] + [string]$Objects, + + [Parameter(Mandatory=$false)] + [string]$Extension, + + [Parameter(Mandatory=$false)] + [switch]$AllExtensions, + + [Parameter(Mandatory=$false)] + [ValidateSet("Hierarchical", "Plain")] + [string]$Format = "Hierarchical" +) + +$OutputEncoding = [System.Text.Encoding]::UTF8 +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- Resolve V8Path --- +if (-not $V8Path) { + $found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1 + if ($found) { + $V8Path = $found.FullName + } else { + Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red + exit 1 + } +} elseif (Test-Path $V8Path -PathType Container) { + $V8Path = Join-Path $V8Path "1cv8.exe" +} + +if (-not (Test-Path $V8Path)) { + Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red + exit 1 +} + +# --- Validate connection --- +if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) { + Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red + exit 1 +} + +# --- Validate Partial mode --- +if ($Mode -eq "Partial" -and -not $Objects) { + Write-Host "Error: -Objects required for Partial mode" -ForegroundColor Red + exit 1 +} + +# --- Create output dir if needed --- +if (-not (Test-Path $ConfigDir)) { + New-Item -ItemType Directory -Path $ConfigDir -Force | Out-Null + Write-Host "Created output directory: $ConfigDir" +} + +# --- Temp dir --- +$tempDir = Join-Path $env:TEMP "db_dump_xml_$(Get-Random)" +New-Item -ItemType Directory -Path $tempDir -Force | Out-Null + +try { + # --- Build arguments --- + $arguments = @("DESIGNER") + + if ($InfoBaseServer -and $InfoBaseRef) { + $arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`"" + } else { + $arguments += "/F", "`"$InfoBasePath`"" + } + + if ($UserName) { $arguments += "/N`"$UserName`"" } + if ($Password) { $arguments += "/P`"$Password`"" } + + $arguments += "/DumpConfigToFiles", "`"$ConfigDir`"" + $arguments += "-Format", $Format + + switch ($Mode) { + "Full" { + Write-Host "Executing full configuration dump..." + } + "Changes" { + Write-Host "Executing incremental configuration dump..." + $arguments += "-update" + $arguments += "-force" + } + "Partial" { + Write-Host "Executing partial configuration dump..." + $objectList = $Objects -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ } + + $listFile = Join-Path $tempDir "dump_list.txt" + $utf8Bom = New-Object System.Text.UTF8Encoding($true) + [System.IO.File]::WriteAllLines($listFile, $objectList, $utf8Bom) + + $arguments += "-listFile", "`"$listFile`"" + Write-Host "Objects to dump: $($objectList.Count)" + foreach ($obj in $objectList) { Write-Host " $obj" } + } + "UpdateInfo" { + Write-Host "Updating ConfigDumpInfo.xml..." + $arguments += "-configDumpInfoOnly" + } + } + + # --- Extensions --- + if ($Extension) { + $arguments += "-Extension", "`"$Extension`"" + } elseif ($AllExtensions) { + $arguments += "-AllExtensions" + } + + # --- Output --- + $outFile = Join-Path $tempDir "dump_log.txt" + $arguments += "/Out", "`"$outFile`"" + $arguments += "/DisableStartupDialogs" + + # --- Execute --- + Write-Host "Running: 1cv8.exe $($arguments -join ' ')" + $process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru + $exitCode = $process.ExitCode + + # --- Result --- + if ($exitCode -eq 0) { + Write-Host "Dump completed successfully" -ForegroundColor Green + Write-Host "Configuration dumped to: $ConfigDir" + } else { + Write-Host "Error dumping configuration (code: $exitCode)" -ForegroundColor Red + } + + if (Test-Path $outFile) { + $logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue + if ($logContent) { + Write-Host "--- Log ---" + Write-Host $logContent + Write-Host "--- End ---" + } + } + + exit $exitCode + +} finally { + if (Test-Path $tempDir) { + Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue + } +} diff --git a/.codex/skills/db-dump-xml/scripts/db-dump-xml.py b/.codex/skills/db-dump-xml/scripts/db-dump-xml.py new file mode 100644 index 00000000..656153b6 --- /dev/null +++ b/.codex/skills/db-dump-xml/scripts/db-dump-xml.py @@ -0,0 +1,173 @@ +#!/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(): + sys.stdout.reconfigure(encoding="utf-8") + sys.stderr.reconfigure(encoding="utf-8") + 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", args.InfoBasePath] + + if args.UserName: + arguments.append(f"/N{args.UserName}") + if args.Password: + arguments.append(f"/P{args.Password}") + + arguments += ["/DumpConfigToFiles", 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", 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", args.Extension] + elif args.AllExtensions: + arguments.append("-AllExtensions") + + # --- Output --- + out_file = os.path.join(temp_dir, "dump_log.txt") + arguments += ["/Out", 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/.codex/skills/db-list/SKILL.md b/.codex/skills/db-list/SKILL.md new file mode 100644 index 00000000..41e0cb26 --- /dev/null +++ b/.codex/skills/db-list/SKILL.md @@ -0,0 +1,158 @@ +--- +name: db-list +description: Управление реестром баз данных 1С (.v8-project.json). Используй когда нужно работать с реестром баз — список баз, зарегистрировать базу в реестре, какие базы есть +argument-hint: "[add|remove|show]" +allowed-tools: + - Read + - Write + - Glob + - AskUserQuestion +--- + +# /db-list — Управление реестром баз данных + +Управляет файлом `.v8-project.json` — реестром информационных баз проекта. Файл хранит параметры подключения, алиасы, привязку к веткам Git. + +## Usage + +``` +/db-list — показать список баз +/db-list add — добавить базу (интерактивно) +/db-list remove — удалить базу из реестра +/db-list show — подробности по базе +``` + +## Формат `.v8-project.json` + +Файл размещается в корне проекта (рядом с `.git/`). + +```json +{ + "v8path": "C:\\Program Files\\1cv8\\8.3.25.1257\\bin", + "databases": [ + { + "id": "dev", + "name": "Разработка", + "type": "file", + "path": "C:\\Bases\\MyApp_Dev", + "user": "Admin", + "password": "", + "aliases": ["dev", "разработка"], + "branches": ["dev", "develop", "feature/*"], + "configSrc": "C:\\WS\\myapp\\cfsrc" + }, + { + "id": "test", + "name": "Тестовая", + "type": "server", + "server": "srv01", + "ref": "MyApp_Test", + "user": "Admin", + "password": "123", + "aliases": ["test", "тест"] + } + ], + "default": "dev" +} +``` + +### Поля корневого объекта + +| Поле | Тип | Описание | +|------|-----|----------| +| `v8path` | string | Каталог bin платформы 1С. Необязательный — если не задан, автоопределение | +| `databases` | array | Массив баз данных | +| `default` | string | id базы по умолчанию | + +### Поля объекта базы данных + +| Поле | Тип | Обязательное | Описание | +|------|-----|:------------:|----------| +| `id` | string | да | Уникальный идентификатор (латиница, без пробелов) | +| `name` | string | да | Человекочитаемое имя | +| `type` | `"file"` / `"server"` | да | Тип подключения | +| `path` | string | для file | Путь к каталогу файловой базы | +| `server` | string | для server | Адрес сервера 1С | +| `ref` | string | для server | Имя базы на сервере | +| `user` | string | нет | Имя пользователя 1С | +| `password` | string | нет | Пароль | +| `aliases` | string[] | нет | Альтернативные имена для быстрого доступа | +| `branches` | string[] | нет | Git-ветки или glob-паттерны (`release/*`, `feature/*`), привязанные к этой базе | +| `configSrc` | string | нет | Каталог XML-выгрузки конфигурации | + +## Алгоритм разрешения базы данных + +Этот алгоритм используется ВСЕМИ навыками (`db-*`, `epf-build`, `epf-dump`, `erf-build`, `erf-dump`) для определения целевой базы. + +1. Если пользователь указал **параметры подключения** (путь, сервер) — используй напрямую +2. Если пользователь указал **базу по имени** — ищи совпадение в таком порядке: + 1. По `id` (точное совпадение) + 2. По `aliases` (совпадение в массиве с учётом морфологии: «тестовую» = «тестовая» = «тестовой») + 3. По `name` (нечёткое совпадение с учётом морфологии и регистра) +3. Если пользователь **не указал** базу — сопоставь текущую ветку Git с `databases[].branches`: + - Точное совпадение: ветка `dev` → `"branches": ["dev"]` + - Glob-паттерн: ветка `release/2.1` → `"branches": ["release/*"]` +4. Если ветка не совпала — используй `default` +5. Если не найдено или неоднозначно — спроси пользователя +6. Если файл `.v8-project.json` не найден — спроси параметры подключения и предложи создать файл + +После выполнения: если использованная база не зарегистрирована — предложи добавить через `/db-list add`. + +### Автоопределение платформы + +Если `v8path` не задан в конфиге: + +```powershell +$v8 = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort-Object -Descending | Select-Object -First 1 +``` + +## Операции + +### Показать список баз + +Прочитай `.v8-project.json`, выведи таблицу: + +``` +ID Имя Тип Путь/Сервер По умолч. +dev Разработка file C:\Bases\MyApp_Dev ✓ +test Тестовая server srv01/MyApp_Test +``` + +### Добавить базу + +Спроси у пользователя через AskUserQuestion: +- id, name, type (file/server) +- path (для file) или server + ref (для server) +- user, password (необязательно) +- aliases, branches (необязательно) + +Добавь в массив `databases`. Если это первая база — установи как `default`. + +### Удалить базу + +Удали из массива `databases` по id. Если удаляемая была `default` — спросить новый default. + +### Подробности по базе + +Выведи все поля конкретной базы. + +## Формирование строки подключения + +Для использования в шаблонах команд других навыков: + +**Файловая база:** +``` +/F "" +``` + +**Серверная база:** +``` +/S "/" +``` + +**Аутентификация** (добавляется если user задан): +``` +/N"" /P"" +``` + +> **Важно**: между `/N` и именем пробела нет. Между `/P` и паролем пробела нет. Если пароль пустой — опусти `/P` целиком. diff --git a/.codex/skills/db-load-cf/SKILL.md b/.codex/skills/db-load-cf/SKILL.md new file mode 100644 index 00000000..94776f6a --- /dev/null +++ b/.codex/skills/db-load-cf/SKILL.md @@ -0,0 +1,81 @@ +--- +name: db-load-cf +description: Загрузка конфигурации 1С из CF-файла. Используй когда нужно загрузить конфигурацию из CF, восстановить из бэкапа CF +argument-hint: [database] +allowed-tools: + - Bash + - Read + - Glob + - AskUserQuestion +--- + +# /db-load-cf — Загрузка конфигурации из CF-файла + +Загружает конфигурацию из бинарного CF-файла в информационную базу. + +## Usage + +``` +/db-load-cf [database] +/db-load-cf config.cf dev +``` + +> **Внимание**: загрузка CF **полностью заменяет** конфигурацию в базе. Перед выполнением запроси подтверждение у пользователя. + +## Параметры подключения + +Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу: +1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую +2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` +3. Если не указал — сопоставь текущую ветку Git с `databases[].branches` +4. Если ветка не совпала — используй `default` +Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1` +Если файла нет — предложи `/db-list add`. +Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. + +## Команда + +```powershell +powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/db-load-cf/scripts/db-load-cf.ps1" <параметры> +``` + +### Параметры скрипта + +| Параметр | Обязательный | Описание | +|----------|:------------:|----------| +| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) | +| `-InfoBasePath <путь>` | * | Файловая база | +| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) | +| `-InfoBaseRef <имя>` | * | Имя базы на сервере | +| `-UserName <имя>` | нет | Имя пользователя | +| `-Password <пароль>` | нет | Пароль | +| `-InputFile <путь>` | да | Путь к CF-файлу | +| `-Extension <имя>` | нет | Загрузить как расширение | +| `-AllExtensions` | нет | Загрузить все расширения из архива | + +> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef` + +## Коды возврата + +| Код | Описание | +|-----|----------| +| 0 | Успешно | +| 1 | Ошибка (см. лог) | + +## После выполнения + +1. Прочитай лог-файл и покажи результат +2. **Предложи выполнить `/db-update`** — загрузка CF обновляет только «основную» конфигурацию конфигуратора, для применения к БД нужен `/UpdateDBCfg` + +## Примеры + +```powershell +# Файловая база +powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/db-load-cf/scripts/db-load-cf.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "C:\backup\config.cf" + +# Серверная база +powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/db-load-cf/scripts/db-load-cf.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test" -UserName "Admin" -Password "secret" -InputFile "config.cf" + +# Загрузка расширения +powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/db-load-cf/scripts/db-load-cf.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "ext.cfe" -Extension "МоёРасширение" +``` diff --git a/.codex/skills/db-load-cf/scripts/db-load-cf.ps1 b/.codex/skills/db-load-cf/scripts/db-load-cf.ps1 new file mode 100644 index 00000000..af516923 --- /dev/null +++ b/.codex/skills/db-load-cf/scripts/db-load-cf.ps1 @@ -0,0 +1,166 @@ +# db-load-cf v1.0 — Load 1C configuration from CF file +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +<# +.SYNOPSIS + Загрузка конфигурации 1С из CF-файла + +.DESCRIPTION + Загружает конфигурацию из бинарного CF-файла в информационную базу. + Поддерживает загрузку расширений. + +.PARAMETER V8Path + Путь к каталогу bin платформы или к 1cv8.exe + +.PARAMETER InfoBasePath + Путь к файловой информационной базе + +.PARAMETER InfoBaseServer + Сервер 1С (для серверной базы) + +.PARAMETER InfoBaseRef + Имя базы на сервере + +.PARAMETER UserName + Имя пользователя 1С + +.PARAMETER Password + Пароль пользователя + +.PARAMETER InputFile + Путь к CF-файлу для загрузки + +.PARAMETER Extension + Загрузить как расширение + +.PARAMETER AllExtensions + Загрузить все расширения из архива + +.EXAMPLE + .\db-load-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "config.cf" + +.EXAMPLE + .\db-load-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "ext.cfe" -Extension "МоёРасширение" +#> + +[CmdletBinding()] +param( + [Parameter(Mandatory=$false)] + [string]$V8Path, + + [Parameter(Mandatory=$false)] + [string]$InfoBasePath, + + [Parameter(Mandatory=$false)] + [string]$InfoBaseServer, + + [Parameter(Mandatory=$false)] + [string]$InfoBaseRef, + + [Parameter(Mandatory=$false)] + [string]$UserName, + + [Parameter(Mandatory=$false)] + [string]$Password, + + [Parameter(Mandatory=$true)] + [string]$InputFile, + + [Parameter(Mandatory=$false)] + [string]$Extension, + + [Parameter(Mandatory=$false)] + [switch]$AllExtensions +) + +$OutputEncoding = [System.Text.Encoding]::UTF8 +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- Resolve V8Path --- +if (-not $V8Path) { + $found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1 + if ($found) { + $V8Path = $found.FullName + } else { + Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red + exit 1 + } +} elseif (Test-Path $V8Path -PathType Container) { + $V8Path = Join-Path $V8Path "1cv8.exe" +} + +if (-not (Test-Path $V8Path)) { + Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red + exit 1 +} + +# --- Validate connection --- +if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) { + Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red + exit 1 +} + +# --- Validate input file --- +if (-not (Test-Path $InputFile)) { + Write-Host "Error: input file not found: $InputFile" -ForegroundColor Red + exit 1 +} + +# --- Temp dir --- +$tempDir = Join-Path $env:TEMP "db_load_cf_$(Get-Random)" +New-Item -ItemType Directory -Path $tempDir -Force | Out-Null + +try { + # --- Build arguments --- + $arguments = @("DESIGNER") + + if ($InfoBaseServer -and $InfoBaseRef) { + $arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`"" + } else { + $arguments += "/F", "`"$InfoBasePath`"" + } + + if ($UserName) { $arguments += "/N`"$UserName`"" } + if ($Password) { $arguments += "/P`"$Password`"" } + + $arguments += "/LoadCfg", "`"$InputFile`"" + + # --- Extensions --- + if ($Extension) { + $arguments += "-Extension", "`"$Extension`"" + } elseif ($AllExtensions) { + $arguments += "-AllExtensions" + } + + # --- Output --- + $outFile = Join-Path $tempDir "load_cf_log.txt" + $arguments += "/Out", "`"$outFile`"" + $arguments += "/DisableStartupDialogs" + + # --- Execute --- + Write-Host "Running: 1cv8.exe $($arguments -join ' ')" + $process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru + $exitCode = $process.ExitCode + + # --- Result --- + if ($exitCode -eq 0) { + Write-Host "Configuration loaded successfully from: $InputFile" -ForegroundColor Green + } else { + Write-Host "Error loading configuration (code: $exitCode)" -ForegroundColor Red + } + + if (Test-Path $outFile) { + $logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue + if ($logContent) { + Write-Host "--- Log ---" + Write-Host $logContent + Write-Host "--- End ---" + } + } + + exit $exitCode + +} finally { + if (Test-Path $tempDir) { + Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue + } +} diff --git a/.codex/skills/db-load-cf/scripts/db-load-cf.py b/.codex/skills/db-load-cf/scripts/db-load-cf.py new file mode 100644 index 00000000..991b11fc --- /dev/null +++ b/.codex/skills/db-load-cf/scripts/db-load-cf.py @@ -0,0 +1,128 @@ +#!/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(): + sys.stdout.reconfigure(encoding="utf-8") + sys.stderr.reconfigure(encoding="utf-8") + 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", args.InfoBasePath]) + + if args.UserName: + arguments.append(f"/N{args.UserName}") + if args.Password: + arguments.append(f"/P{args.Password}") + + arguments.extend(["/LoadCfg", args.InputFile]) + + # --- Extensions --- + if args.Extension: + arguments.extend(["-Extension", args.Extension]) + elif args.AllExtensions: + arguments.append("-AllExtensions") + + # --- Output --- + out_file = os.path.join(temp_dir, "load_cf_log.txt") + arguments.extend(["/Out", 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/.codex/skills/db-load-git/SKILL.md b/.codex/skills/db-load-git/SKILL.md new file mode 100644 index 00000000..ec2438f3 --- /dev/null +++ b/.codex/skills/db-load-git/SKILL.md @@ -0,0 +1,78 @@ +--- +name: db-load-git +description: Загрузка изменений из Git в базу 1С. Используй когда нужно загрузить изменения из гита, обновить базу из репозитория, partial load из коммита +argument-hint: "[database] [source]" +allowed-tools: + - Bash + - Read + - Glob + - AskUserQuestion +--- + +# /db-load-git — Загрузка изменений из Git + +Определяет изменённые файлы конфигурации по данным Git и выполняет частичную загрузку в информационную базу. + +## Usage + +``` +/db-load-git [database] +/db-load-git dev — все незафиксированные изменения +/db-load-git dev -Source Staged — только staged +/db-load-git dev -Source Commit -CommitRange "HEAD~3..HEAD" +/db-load-git dev -DryRun — только показать что будет загружено +``` + +## Параметры подключения + +Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу: +1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую +2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` +3. Если не указал — сопоставь текущую ветку Git с `databases[].branches` +4. Если ветка не совпала — используй `default` +Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1` +Если файла нет — предложи `/db-list add`. +Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. +Если в записи базы указан `configSrc` — используй как каталог конфигурации. + +## Команда + +```powershell +powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/db-load-git/scripts/db-load-git.ps1" <параметры> +``` + +### Параметры скрипта + +| Параметр | Обязательный | Описание | +|----------|:------------:|----------| +| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) | +| `-InfoBasePath <путь>` | * | Файловая база | +| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) | +| `-InfoBaseRef <имя>` | * | Имя базы на сервере | +| `-UserName <имя>` | нет | Имя пользователя | +| `-Password <пароль>` | нет | Пароль | +| `-ConfigDir <путь>` | да | Каталог XML-выгрузки (git-репозиторий) | +| `-Source <источник>` | нет | `All` (по умолч.) / `Staged` / `Unstaged` / `Commit` | +| `-CommitRange ` | для Commit | Диапазон коммитов (напр. `HEAD~3..HEAD`) | +| `-Extension <имя>` | нет | Загрузить в расширение | +| `-AllExtensions` | нет | Загрузить все расширения | +| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` | +| `-DryRun` | нет | Только показать что будет загружено (без загрузки) | +| `-UpdateDB` | нет | После загрузки сразу обновить конфигурацию БД (`/UpdateDBCfg`) | + +> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef` + +## После выполнения + +1. Показать список загруженных файлов и результат из лога +2. Если `-UpdateDB` не был указан — **предложить `/db-update`** для применения изменений к БД + +## Примеры + +```powershell +# Все незафиксированные изменения +powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/db-load-git/scripts/db-load-git.ps1" -V8Path "C:\Program Files\1cv8\8.3.25.1257\bin" -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\WS\cfsrc" -Source All -UpdateDB + +# Из диапазона коммитов +powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/db-load-git/scripts/db-load-git.ps1" -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\WS\cfsrc" -Source Commit -CommitRange "HEAD~3..HEAD" +``` diff --git a/.codex/skills/db-load-git/scripts/db-load-git.ps1 b/.codex/skills/db-load-git/scripts/db-load-git.ps1 new file mode 100644 index 00000000..576e7ced --- /dev/null +++ b/.codex/skills/db-load-git/scripts/db-load-git.ps1 @@ -0,0 +1,359 @@ +# db-load-git v1.3 — Load Git changes into 1C database +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +<# +.SYNOPSIS + Загрузка изменений из Git в базу 1С + +.DESCRIPTION + Определяет изменённые файлы конфигурации по данным Git и выполняет + частичную загрузку в информационную базу. + +.PARAMETER V8Path + Путь к каталогу bin платформы или к 1cv8.exe + +.PARAMETER InfoBasePath + Путь к файловой информационной базе + +.PARAMETER InfoBaseServer + Сервер 1С (для серверной базы) + +.PARAMETER InfoBaseRef + Имя базы на сервере + +.PARAMETER UserName + Имя пользователя 1С + +.PARAMETER Password + Пароль пользователя + +.PARAMETER ConfigDir + Каталог XML-выгрузки конфигурации (git-репозиторий) + +.PARAMETER Source + Источник изменений: All, Staged, Unstaged, Commit (по умолчанию All) + +.PARAMETER CommitRange + Диапазон коммитов (для Source=Commit), напр. HEAD~3..HEAD + +.PARAMETER Extension + Имя расширения для загрузки + +.PARAMETER AllExtensions + Загрузить все расширения + +.PARAMETER Format + Формат файлов: Hierarchical или Plain (по умолчанию Hierarchical) + +.PARAMETER DryRun + Только показать что будет загружено (без загрузки) + +.EXAMPLE + .\db-load-git.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Source All + +.EXAMPLE + .\db-load-git.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Source Commit -CommitRange "HEAD~3..HEAD" + +.EXAMPLE + .\db-load-git.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -DryRun +#> + +[CmdletBinding()] +param( + [Parameter(Mandatory=$false)] + [string]$V8Path, + + [Parameter(Mandatory=$false)] + [string]$InfoBasePath, + + [Parameter(Mandatory=$false)] + [string]$InfoBaseServer, + + [Parameter(Mandatory=$false)] + [string]$InfoBaseRef, + + [Parameter(Mandatory=$false)] + [string]$UserName, + + [Parameter(Mandatory=$false)] + [string]$Password, + + [Parameter(Mandatory=$true)] + [string]$ConfigDir, + + [Parameter(Mandatory=$false)] + [ValidateSet("All", "Staged", "Unstaged", "Commit")] + [string]$Source = "All", + + [Parameter(Mandatory=$false)] + [string]$CommitRange, + + [Parameter(Mandatory=$false)] + [string]$Extension, + + [Parameter(Mandatory=$false)] + [switch]$AllExtensions, + + [Parameter(Mandatory=$false)] + [ValidateSet("Hierarchical", "Plain")] + [string]$Format = "Hierarchical", + + [Parameter(Mandatory=$false)] + [switch]$DryRun, + + [Parameter(Mandatory=$false)] + [switch]$UpdateDB +) + +$OutputEncoding = [System.Text.Encoding]::UTF8 +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- Helper: map sub-file path (BSL, HTML, etc.) to object XML --- +function Get-ObjectXmlFromSubFile { + param([string]$RelativePath) + + $parts = $RelativePath -split '[\\/]' + if ($parts.Count -ge 2) { + return "$($parts[0])/$($parts[1]).xml" + } + return $null +} + +# --- Resolve V8Path (skip if DryRun) --- +if (-not $DryRun) { + if (-not $V8Path) { + $found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1 + if ($found) { + $V8Path = $found.FullName + } else { + Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red + exit 1 + } + } elseif (Test-Path $V8Path -PathType Container) { + $V8Path = Join-Path $V8Path "1cv8.exe" + } + + if (-not (Test-Path $V8Path)) { + Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red + exit 1 + } +} + +# --- Validate connection (skip if DryRun) --- +if (-not $DryRun) { + if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) { + Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red + exit 1 + } +} + +# --- Validate config dir --- +if (-not (Test-Path $ConfigDir)) { + Write-Host "Error: config directory not found: $ConfigDir" -ForegroundColor Red + exit 1 +} + +# --- Validate Commit mode --- +if ($Source -eq "Commit" -and -not $CommitRange) { + Write-Host "Error: -CommitRange required for Source=Commit" -ForegroundColor Red + exit 1 +} + +# --- Check git --- +try { + $null = git --version 2>&1 +} catch { + Write-Host "Error: git not found in PATH" -ForegroundColor Red + exit 1 +} + +# --- Get changed files from Git --- +$changedFiles = @() +$ConfigDir = (Resolve-Path $ConfigDir).Path.TrimEnd('\') +$configDirNormalized = $ConfigDir.Replace('\', '/') + +Push-Location $ConfigDir +try { + switch ($Source) { + "Staged" { + Write-Host "Getting staged changes..." + $raw = git diff --cached --name-only --relative 2>&1 + if ($LASTEXITCODE -eq 0) { $changedFiles += $raw } + } + "Unstaged" { + Write-Host "Getting unstaged changes..." + $raw = git diff --name-only --relative 2>&1 + if ($LASTEXITCODE -eq 0) { $changedFiles += $raw } + $raw = git ls-files --others --exclude-standard 2>&1 + if ($LASTEXITCODE -eq 0) { $changedFiles += $raw } + } + "Commit" { + Write-Host "Getting changes from $CommitRange..." + $raw = git diff --name-only --relative $CommitRange 2>&1 + if ($LASTEXITCODE -eq 0) { $changedFiles += $raw } + } + "All" { + Write-Host "Getting all uncommitted changes..." + $raw = git diff --cached --name-only --relative 2>&1 + if ($LASTEXITCODE -eq 0) { $changedFiles += $raw } + $raw = git diff --name-only --relative 2>&1 + if ($LASTEXITCODE -eq 0) { $changedFiles += $raw } + $raw = git ls-files --others --exclude-standard 2>&1 + if ($LASTEXITCODE -eq 0) { $changedFiles += $raw } + } + } +} finally { + Pop-Location +} + +$changedFiles = $changedFiles | Where-Object { $_ -is [string] -and -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique + +if ($changedFiles.Count -eq 0) { + Write-Host "No changes found" + exit 0 +} + +Write-Host "Git changes detected: $($changedFiles.Count) files" + +# --- Filter and map to config files --- +$configFiles = @() + +foreach ($file in $changedFiles) { + $file = $file.Trim().Replace('\', '/') + if ([string]::IsNullOrWhiteSpace($file)) { continue } + + # Skip service files + if ($file -eq "ConfigDumpInfo.xml") { continue } + + $fullPath = Join-Path $ConfigDir $file + + if ($file -match '\.xml$') { + # XML file — add directly if exists + if (Test-Path $fullPath) { + if ($configFiles -notcontains $file) { + $configFiles += $file + } + } + } + else { + # Non-XML (BSL, HTML, etc.) — map to parent object XML + include all Ext/ files + $objectXml = Get-ObjectXmlFromSubFile -RelativePath $file + if ($objectXml) { + $fullXmlPath = Join-Path $ConfigDir $objectXml + if (Test-Path $fullXmlPath) { + if ($configFiles -notcontains $objectXml) { + $configFiles += $objectXml + } + if ((Test-Path $fullPath) -and $configFiles -notcontains $file) { + $configFiles += $file + } + + # Add all files from Ext/ directory of the object + $parts = $file -split '[\\/]' + if ($parts.Count -ge 2) { + $extDir = Join-Path (Join-Path $ConfigDir $parts[0]) "$($parts[1])\Ext" + if (Test-Path $extDir) { + Get-ChildItem -Path $extDir -Recurse -File | ForEach-Object { + $extRelPath = $_.FullName.Replace("$ConfigDir\", '').Replace('\', '/') + if ($configFiles -notcontains $extRelPath) { + $configFiles += $extRelPath + } + } + } + } + } + } + } +} + +if ($configFiles.Count -eq 0) { + Write-Host "No configuration files found in changes" + exit 0 +} + +Write-Host "Files for loading: $($configFiles.Count)" +foreach ($f in $configFiles) { Write-Host " $f" } + +# --- DryRun: stop here --- +if ($DryRun) { + Write-Host "" + Write-Host "DryRun mode - no changes applied" + exit 0 +} + +# --- Temp dir --- +$tempDir = Join-Path $env:TEMP "db_load_git_$(Get-Random)" +New-Item -ItemType Directory -Path $tempDir -Force | Out-Null + +try { + # --- Write list file (UTF-8 with BOM) --- + $listFile = Join-Path $tempDir "load_list.txt" + $utf8Bom = New-Object System.Text.UTF8Encoding($true) + [System.IO.File]::WriteAllLines($listFile, $configFiles, $utf8Bom) + + # --- Build arguments --- + $arguments = @("DESIGNER") + + if ($InfoBaseServer -and $InfoBaseRef) { + $arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`"" + } else { + $arguments += "/F", "`"$InfoBasePath`"" + } + + if ($UserName) { $arguments += "/N`"$UserName`"" } + if ($Password) { $arguments += "/P`"$Password`"" } + + $arguments += "/LoadConfigFromFiles", "`"$ConfigDir`"" + $arguments += "-listFile", "`"$listFile`"" + $arguments += "-Format", $Format + $arguments += "-partial" + $arguments += "-updateConfigDumpInfo" + + # --- Extensions --- + if ($Extension) { + $arguments += "-Extension", "`"$Extension`"" + } elseif ($AllExtensions) { + $arguments += "-AllExtensions" + } + + # --- UpdateDB --- + if ($UpdateDB) { + $arguments += "/UpdateDBCfg" + } + + # --- Output --- + $outFile = Join-Path $tempDir "load_log.txt" + $arguments += "/Out", "`"$outFile`"" + $arguments += "/DisableStartupDialogs" + + # --- Execute --- + Write-Host "" + Write-Host "Executing partial configuration load..." + Write-Host "Running: 1cv8.exe $($arguments -join ' ')" + + $process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru + $exitCode = $process.ExitCode + + # --- Result --- + Write-Host "" + if ($exitCode -eq 0) { + Write-Host "Load completed successfully" -ForegroundColor Green + } else { + Write-Host "Error loading configuration (code: $exitCode)" -ForegroundColor Red + } + + if (Test-Path $outFile) { + $logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue + if ($logContent) { + Write-Host "--- Log ---" + Write-Host $logContent + Write-Host "--- End ---" + } + } + + exit $exitCode + +} finally { + if (Test-Path $tempDir) { + Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue + } +} diff --git a/.codex/skills/db-load-git/scripts/db-load-git.py b/.codex/skills/db-load-git/scripts/db-load-git.py new file mode 100644 index 00000000..c032e9c0 --- /dev/null +++ b/.codex/skills/db-load-git/scripts/db-load-git.py @@ -0,0 +1,285 @@ +#!/usr/bin/env python3 +# db-load-git v1.3 — 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_subfile(relative_path): + """Map sub-file path (BSL, HTML, etc.) 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, + encoding="utf-8", + cwd=config_dir, + ) + if result.returncode == 0: + return [line for line in result.stdout.splitlines() if line.strip()] + return [] + + +def main(): + sys.stdout.reconfigure(encoding="utf-8") + sys.stderr.reconfigure(encoding="utf-8") + 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)") + parser.add_argument("-UpdateDB", action="store_true", help="Also update database configuration after 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", "--relative"]) + elif args.Source == "Unstaged": + print("Getting unstaged changes...") + changed_files += run_git(args.ConfigDir, ["diff", "--name-only", "--relative"]) + 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", "--relative", args.CommitRange]) + elif args.Source == "All": + print("Getting all uncommitted changes...") + changed_files += run_git(args.ConfigDir, ["diff", "--cached", "--name-only", "--relative"]) + changed_files += run_git(args.ConfigDir, ["diff", "--name-only", "--relative"]) + 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 + + full_path = os.path.join(args.ConfigDir, file) + + if file.endswith(".xml"): + # XML file — add directly if exists + if os.path.exists(full_path): + if file not in config_files: + config_files.append(file) + else: + # Non-XML (BSL, HTML, etc.) — map to parent object XML + include all Ext/ files + object_xml = get_object_xml_from_subfile(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 os.path.exists(full_path) and 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) + 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", args.InfoBasePath] + + if args.UserName: + arguments.append(f"/N{args.UserName}") + if args.Password: + arguments.append(f"/P{args.Password}") + + arguments += ["/LoadConfigFromFiles", args.ConfigDir] + arguments += ["-listFile", list_file] + arguments += ["-Format", args.Format] + arguments.append("-partial") + arguments.append("-updateConfigDumpInfo") + + # --- Extensions --- + if args.Extension: + arguments += ["-Extension", args.Extension] + elif args.AllExtensions: + arguments.append("-AllExtensions") + + # --- UpdateDB --- + if args.UpdateDB: + arguments.append("/UpdateDBCfg") + + # --- Output --- + out_file = os.path.join(temp_dir, "load_log.txt") + arguments += ["/Out", 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/.codex/skills/db-load-xml/SKILL.md b/.codex/skills/db-load-xml/SKILL.md new file mode 100644 index 00000000..6f8b987e --- /dev/null +++ b/.codex/skills/db-load-xml/SKILL.md @@ -0,0 +1,109 @@ +--- +name: db-load-xml +description: Загрузка конфигурации 1С из XML-файлов. Используй когда нужно загрузить конфигурацию из файлов, XML, исходников, LoadConfigFromFiles +argument-hint: [database] +allowed-tools: + - Bash + - Read + - Glob + - AskUserQuestion +--- + +# /db-load-xml — Загрузка конфигурации из XML + +Загружает конфигурацию в информационную базу из XML-файлов (исходников). Поддерживает полную и частичную загрузку. + +## Usage + +``` +/db-load-xml [database] +/db-load-xml src/config dev +/db-load-xml src/config dev -Mode Partial -Files "Catalogs/Номенклатура.xml,Catalogs/Номенклатура/Ext/ObjectModule.bsl" +``` + +> **Внимание**: полная загрузка **заменяет всю конфигурацию** в базе. Перед выполнением запроси подтверждение у пользователя. + +## Параметры подключения + +Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу: +1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую +2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` +3. Если не указал — сопоставь текущую ветку Git с `databases[].branches` +4. Если ветка не совпала — используй `default` +Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1` +Если файла нет — предложи `/db-list add`. +Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. +Если в записи базы указан `configSrc` — используй как каталог загрузки по умолчанию. + +## Команда + +```powershell +powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/db-load-xml/scripts/db-load-xml.ps1" <параметры> +``` + +### Параметры скрипта + +| Параметр | Обязательный | Описание | +|----------|:------------:|----------| +| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) | +| `-InfoBasePath <путь>` | * | Файловая база | +| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) | +| `-InfoBaseRef <имя>` | * | Имя базы на сервере | +| `-UserName <имя>` | нет | Имя пользователя | +| `-Password <пароль>` | нет | Пароль | +| `-ConfigDir <путь>` | да | Каталог XML-исходников | +| `-Mode <режим>` | нет | `Full` (по умолч.) / `Partial` | +| `-Files <список>` | для Partial | Относительные пути файлов через запятую | +| `-ListFile <путь>` | для Partial | Путь к файлу со списком (альтернатива `-Files`) | +| `-Extension <имя>` | нет | Загрузить в расширение | +| `-AllExtensions` | нет | Загрузить все расширения | +| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` | +| `-UpdateDB` | нет | После загрузки сразу обновить конфигурацию БД (`/UpdateDBCfg`) | + +> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef` + +### Режимы загрузки + +| Режим | Описание | +|-------|----------| +| `Full` | Полная загрузка — замена всей конфигурации из каталога XML | +| `Partial` | Частичная — загрузка выбранных файлов (с `-partial -updateConfigDumpInfo`) | + +### Формат файла списка (listFile) + +Файл содержит **относительные пути к файлам** в каталоге выгрузки (один на строку), кодировка **UTF-8 с BOM**: + +``` +Catalogs/Номенклатура.xml +Catalogs/Номенклатура/Ext/ObjectModule.bsl +Documents/Заказ.xml +Documents/Заказ/Forms/ФормаДокумента.xml +``` + +## Коды возврата + +| Код | Описание | +|-----|----------| +| 0 | Успешно | +| 1 | Ошибка (см. лог) | + +## После выполнения + +1. Прочитай лог и покажи результат +2. Если `-UpdateDB` не был указан — **предложи выполнить `/db-update`** для применения изменений к БД + +## Примеры + +```powershell +# Полная загрузка +powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/db-load-xml/scripts/db-load-xml.ps1" -V8Path "C:\Program Files\1cv8\8.3.25.1257\bin" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full + +# Частичная загрузка конкретных файлов +powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/db-load-xml/scripts/db-load-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Partial -Files "Catalogs/Номенклатура.xml,Catalogs/Номенклатура/Ext/ObjectModule.bsl" + +# Загрузка расширения +powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/db-load-xml/scripts/db-load-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\ext_src" -Mode Full -Extension "МоёРасширение" + +# Загрузка + обновление БД в одном запуске +powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/db-load-xml/scripts/db-load-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full -UpdateDB +``` diff --git a/.codex/skills/db-load-xml/scripts/db-load-xml.ps1 b/.codex/skills/db-load-xml/scripts/db-load-xml.ps1 new file mode 100644 index 00000000..bdbcd9cf --- /dev/null +++ b/.codex/skills/db-load-xml/scripts/db-load-xml.ps1 @@ -0,0 +1,279 @@ +# db-load-xml v1.3 — Load 1C configuration from XML files +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +<# +.SYNOPSIS + Загрузка конфигурации 1С из XML-файлов + +.DESCRIPTION + Загружает конфигурацию в информационную базу из XML-файлов. + Поддерживает полную и частичную загрузку. + +.PARAMETER V8Path + Путь к каталогу bin платформы или к 1cv8.exe + +.PARAMETER InfoBasePath + Путь к файловой информационной базе + +.PARAMETER InfoBaseServer + Сервер 1С (для серверной базы) + +.PARAMETER InfoBaseRef + Имя базы на сервере + +.PARAMETER UserName + Имя пользователя 1С + +.PARAMETER Password + Пароль пользователя + +.PARAMETER ConfigDir + Каталог XML-исходников конфигурации + +.PARAMETER Mode + Режим загрузки: Full или Partial (по умолчанию Full) + +.PARAMETER Files + Относительные пути файлов через запятую (для режима Partial) + +.PARAMETER ListFile + Путь к файлу со списком файлов (альтернатива -Files, для режима Partial) + +.PARAMETER Extension + Имя расширения для загрузки + +.PARAMETER AllExtensions + Загрузить все расширения + +.PARAMETER Format + Формат файлов: Hierarchical или Plain (по умолчанию Hierarchical) + +.EXAMPLE + .\db-load-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Full + +.EXAMPLE + .\db-load-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Partial -Files "Catalogs/Номенклатура.xml,Catalogs/Номенклатура/Ext/ObjectModule.bsl" +#> + +[CmdletBinding()] +param( + [Parameter(Mandatory=$false)] + [string]$V8Path, + + [Parameter(Mandatory=$false)] + [string]$InfoBasePath, + + [Parameter(Mandatory=$false)] + [string]$InfoBaseServer, + + [Parameter(Mandatory=$false)] + [string]$InfoBaseRef, + + [Parameter(Mandatory=$false)] + [string]$UserName, + + [Parameter(Mandatory=$false)] + [string]$Password, + + [Parameter(Mandatory=$true)] + [string]$ConfigDir, + + [Parameter(Mandatory=$false)] + [ValidateSet("Full", "Partial")] + [string]$Mode = "Full", + + [Parameter(Mandatory=$false)] + [string]$Files, + + [Parameter(Mandatory=$false)] + [string]$ListFile, + + [Parameter(Mandatory=$false)] + [string]$Extension, + + [Parameter(Mandatory=$false)] + [switch]$AllExtensions, + + [Parameter(Mandatory=$false)] + [ValidateSet("Hierarchical", "Plain")] + [string]$Format = "Hierarchical", + + [Parameter(Mandatory=$false)] + [switch]$UpdateDB, + + [Parameter(Mandatory=$false)] + [switch]$StrictLog +) + +$OutputEncoding = [System.Text.Encoding]::UTF8 +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- Resolve V8Path --- +if (-not $V8Path) { + $found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1 + if ($found) { + $V8Path = $found.FullName + } else { + Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red + exit 1 + } +} elseif (Test-Path $V8Path -PathType Container) { + $V8Path = Join-Path $V8Path "1cv8.exe" +} + +if (-not (Test-Path $V8Path)) { + Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red + exit 1 +} + +# --- Validate connection --- +if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) { + Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red + exit 1 +} + +# --- Validate config dir --- +if (-not (Test-Path $ConfigDir)) { + Write-Host "Error: config directory not found: $ConfigDir" -ForegroundColor Red + exit 1 +} + +# --- Validate Partial mode --- +if ($Mode -eq "Partial" -and -not $Files -and -not $ListFile) { + Write-Host "Error: -Files or -ListFile required for Partial mode" -ForegroundColor Red + exit 1 +} + +# --- Temp dir --- +$tempDir = Join-Path $env:TEMP "db_load_xml_$(Get-Random)" +New-Item -ItemType Directory -Path $tempDir -Force | Out-Null + +try { + # --- Build arguments --- + $arguments = @("DESIGNER") + + if ($InfoBaseServer -and $InfoBaseRef) { + $arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`"" + } else { + $arguments += "/F", "`"$InfoBasePath`"" + } + + if ($UserName) { $arguments += "/N`"$UserName`"" } + if ($Password) { $arguments += "/P`"$Password`"" } + + $arguments += "/LoadConfigFromFiles", "`"$ConfigDir`"" + + if ($Mode -eq "Full") { + Write-Host "Executing full configuration load..." + } else { + Write-Host "Executing partial configuration load..." + + # Build list file + $generatedListFile = $null + if ($ListFile) { + # Use provided list file + if (-not (Test-Path $ListFile)) { + Write-Host "Error: list file not found: $ListFile" -ForegroundColor Red + exit 1 + } + $generatedListFile = $ListFile + } else { + # Generate from -Files parameter + $fileList = $Files -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ } + $generatedListFile = Join-Path $tempDir "load_list.txt" + $utf8Bom = New-Object System.Text.UTF8Encoding($true) + [System.IO.File]::WriteAllLines($generatedListFile, $fileList, $utf8Bom) + + Write-Host "Files to load: $($fileList.Count)" + foreach ($f in $fileList) { Write-Host " $f" } + } + + $arguments += "-listFile", "`"$generatedListFile`"" + $arguments += "-partial" + $arguments += "-updateConfigDumpInfo" + } + + $arguments += "-Format", $Format + + # --- Extensions --- + if ($Extension) { + $arguments += "-Extension", "`"$Extension`"" + } elseif ($AllExtensions) { + $arguments += "-AllExtensions" + } + + # --- UpdateDB --- + if ($UpdateDB) { + $arguments += "/UpdateDBCfg" + } + + # --- Output --- + $outFile = Join-Path $tempDir "load_log.txt" + $arguments += "/Out", "`"$outFile`"" + $arguments += "/DisableStartupDialogs" + + # --- Execute --- + Write-Host "Running: 1cv8.exe $($arguments -join ' ')" + $process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru + $exitCode = $process.ExitCode + + # --- Read log --- + $logContent = $null + if (Test-Path $outFile) { + $logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue + } + + # --- Scan log for silent rejections --- + # Platform often writes load-time rejections into /Out but exits with code 0. + # These patterns flag cases where metadata was dropped or rejected silently. + $fatalLogPatterns = @( + 'Неверное свойство объекта метаданных', + 'не входит в состав объекта метаданных', + 'Неизвестное имя типа', + 'Неизвестный объект метаданных', + 'Ни один из документов не является регистратором для регистра', + 'Неверное значение перечисления', + 'не может быть приведен к типу' + ) + $silentFailures = @() + if ($logContent) { + foreach ($line in ($logContent -split "`r?`n")) { + foreach ($pat in $fatalLogPatterns) { + if ($line -match [regex]::Escape($pat)) { + $silentFailures += $line.Trim() + break + } + } + } + } + + # --- Result --- + # Default: mirror platform's verdict via exit code. Log content (including any + # rejection warnings) is always printed to stdout for visibility. With -StrictLog, + # elevate exit code to 1 when rejection patterns are found even if platform said 0. + if ($exitCode -eq 0) { + Write-Host "Load completed successfully" -ForegroundColor Green + } else { + Write-Host "Error loading configuration (code: $exitCode)" -ForegroundColor Red + } + + if ($logContent) { + Write-Host "--- Log ---" + Write-Host $logContent + Write-Host "--- End ---" + } + + if ($silentFailures.Count -gt 0) { + $msg = "[warning] log contains $($silentFailures.Count) rejection(s) — platform loaded config but dropped properties/refs" + if (-not $StrictLog) { $msg += " (pass -StrictLog to treat as error)" } + Write-Host $msg -ForegroundColor Yellow + foreach ($f in $silentFailures) { Write-Host " $f" -ForegroundColor Yellow } + if ($StrictLog -and $exitCode -eq 0) { $exitCode = 1 } + } + + exit $exitCode + +} finally { + if (Test-Path $tempDir) { + Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue + } +} diff --git a/.codex/skills/db-load-xml/scripts/db-load-xml.py b/.codex/skills/db-load-xml/scripts/db-load-xml.py new file mode 100644 index 00000000..e51286b2 --- /dev/null +++ b/.codex/skills/db-load-xml/scripts/db-load-xml.py @@ -0,0 +1,228 @@ +#!/usr/bin/env python3 +# db-load-xml v1.3 — 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(): + sys.stdout.reconfigure(encoding="utf-8") + sys.stderr.reconfigure(encoding="utf-8") + 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)", + ) + parser.add_argument("-UpdateDB", action="store_true", help="Also update database configuration after load") + parser.add_argument( + "-StrictLog", + action="store_true", + help="Treat silent rejection warnings in the log as errors (elevate exit code to 1)", + ) + 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", args.InfoBasePath] + + if args.UserName: + arguments.append(f"/N{args.UserName}") + if args.Password: + arguments.append(f"/P{args.Password}") + + arguments += ["/LoadConfigFromFiles", 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", generated_list_file] + arguments.append("-partial") + arguments.append("-updateConfigDumpInfo") + + arguments += ["-Format", args.Format] + + # --- Extensions --- + if args.Extension: + arguments += ["-Extension", args.Extension] + elif args.AllExtensions: + arguments.append("-AllExtensions") + + # --- UpdateDB --- + if args.UpdateDB: + arguments.append("/UpdateDBCfg") + + # --- Output --- + out_file = os.path.join(temp_dir, "load_log.txt") + arguments += ["/Out", 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 + + # --- Read log --- + log_content = "" + if os.path.isfile(out_file): + try: + with open(out_file, "r", encoding="utf-8-sig") as f: + log_content = f.read() + except Exception: + log_content = "" + + # --- Scan log for silent rejections --- + # Platform often writes load-time rejections into /Out but exits with code 0. + # These patterns flag cases where metadata was dropped or rejected silently. + fatal_log_patterns = [ + "Неверное свойство объекта метаданных", + "не входит в состав объекта метаданных", + "Неизвестное имя типа", + "Неизвестный объект метаданных", + "Ни один из документов не является регистратором для регистра", + "Неверное значение перечисления", + "не может быть приведен к типу", + ] + silent_failures = [] + if log_content: + for line in log_content.splitlines(): + for pat in fatal_log_patterns: + if pat in line: + silent_failures.append(line.strip()) + break + + # --- Result --- + # Default: mirror platform's verdict via exit code. Log content (including any + # rejection warnings) is always printed to stdout for visibility. With -StrictLog, + # elevate exit code to 1 when rejection patterns are found even if platform said 0. + if exit_code == 0: + print("Load completed successfully") + else: + print(f"Error loading configuration (code: {exit_code})", file=sys.stderr) + + if log_content: + print("--- Log ---") + print(log_content) + print("--- End ---") + + if silent_failures: + suffix = "" if args.StrictLog else " (pass -StrictLog to treat as error)" + print( + f"[warning] log contains {len(silent_failures)} rejection(s) — " + f"platform loaded config but dropped properties/refs{suffix}", + file=sys.stderr, + ) + for f in silent_failures: + print(f" {f}", file=sys.stderr) + if args.StrictLog and exit_code == 0: + exit_code = 1 + + 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/.codex/skills/db-run/SKILL.md b/.codex/skills/db-run/SKILL.md new file mode 100644 index 00000000..7c2a7622 --- /dev/null +++ b/.codex/skills/db-run/SKILL.md @@ -0,0 +1,76 @@ +--- +name: db-run +description: Запуск 1С:Предприятие. Используй когда нужно запустить 1С, открыть базу, запустить предприятие +argument-hint: "[database]" +allowed-tools: + - Bash + - Read + - Glob + - AskUserQuestion +--- + +# /db-run — Запуск 1С:Предприятие + +Запускает информационную базу в режиме 1С:Предприятие (пользовательский режим). + +## Usage + +``` +/db-run [database] +/db-run dev +/db-run dev /Execute process.epf +/db-run dev /C "параметр запуска" +``` + +## Параметры подключения + +Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу: +1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую +2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` +3. Если не указал — сопоставь текущую ветку Git с `databases[].branches` +4. Если ветка не совпала — используй `default` +Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1` +Если файла нет — предложи `/db-list add`. +Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. + +## Команда + +```powershell +powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/db-run/scripts/db-run.ps1" <параметры> +``` + +### Параметры скрипта + +| Параметр | Обязательный | Описание | +|----------|:------------:|----------| +| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) | +| `-InfoBasePath <путь>` | * | Файловая база | +| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) | +| `-InfoBaseRef <имя>` | * | Имя базы на сервере | +| `-UserName <имя>` | нет | Имя пользователя | +| `-Password <пароль>` | нет | Пароль | +| `-Execute <файл.epf>` | нет | Запуск внешней обработки сразу после старта | +| `-CParam <строка>` | нет | Параметр запуска (/C) | +| `-URL <ссылка>` | нет | Навигационная ссылка (формат `e1cib/...`) | + +> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef` + +## Важно + +Скрипт запускает 1С в фоне (`Start-Process` без `-Wait`) — управление возвращается сразу. + +## Примеры + +```powershell +# Простой запуск +powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/db-run/scripts/db-run.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" + +# Запуск с обработкой +powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/db-run/scripts/db-run.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -Execute "C:\epf\МояОбработка.epf" + +# Открыть по навигационной ссылке +powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/db-run/scripts/db-run.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -URL "e1cib/data/Справочник.Номенклатура" + +# Серверная база с параметром запуска +powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/db-run/scripts/db-run.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -CParam "ЗапуститьОбновление" +``` diff --git a/.codex/skills/db-run/scripts/db-run.ps1 b/.codex/skills/db-run/scripts/db-run.ps1 new file mode 100644 index 00000000..946291e0 --- /dev/null +++ b/.codex/skills/db-run/scripts/db-run.ps1 @@ -0,0 +1,145 @@ +# db-run v1.0 — Launch 1C:Enterprise +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +<# +.SYNOPSIS + Запуск 1С:Предприятие + +.DESCRIPTION + Запускает информационную базу в режиме 1С:Предприятие (пользовательский режим). + Запуск в фоне — не ждёт завершения процесса. + +.PARAMETER V8Path + Путь к каталогу bin платформы или к 1cv8.exe + +.PARAMETER InfoBasePath + Путь к файловой информационной базе + +.PARAMETER InfoBaseServer + Сервер 1С (для серверной базы) + +.PARAMETER InfoBaseRef + Имя базы на сервере + +.PARAMETER UserName + Имя пользователя 1С + +.PARAMETER Password + Пароль пользователя + +.PARAMETER Execute + Путь к внешней обработке для запуска + +.PARAMETER CParam + Параметр запуска (/C) + +.PARAMETER URL + Навигационная ссылка (e1cib/...) + +.EXAMPLE + .\db-run.ps1 -InfoBasePath "C:\Bases\MyDB" + +.EXAMPLE + .\db-run.ps1 -InfoBasePath "C:\Bases\MyDB" -Execute "C:\epf\МояОбработка.epf" + +.EXAMPLE + .\db-run.ps1 -InfoBasePath "C:\Bases\MyDB" -CParam "ЗапуститьОбновление" +#> + +[CmdletBinding()] +param( + [Parameter(Mandatory=$false)] + [string]$V8Path, + + [Parameter(Mandatory=$false)] + [string]$InfoBasePath, + + [Parameter(Mandatory=$false)] + [string]$InfoBaseServer, + + [Parameter(Mandatory=$false)] + [string]$InfoBaseRef, + + [Parameter(Mandatory=$false)] + [string]$UserName, + + [Parameter(Mandatory=$false)] + [string]$Password, + + [Parameter(Mandatory=$false)] + [string]$Execute, + + [Parameter(Mandatory=$false)] + [string]$CParam, + + [Parameter(Mandatory=$false)] + [string]$URL +) + +$OutputEncoding = [System.Text.Encoding]::UTF8 +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- Resolve V8Path --- +if (-not $V8Path) { + $found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1 + if ($found) { + $V8Path = $found.FullName + } else { + Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red + exit 1 + } +} elseif (Test-Path $V8Path -PathType Container) { + $V8Path = Join-Path $V8Path "1cv8.exe" +} + +if (-not (Test-Path $V8Path)) { + Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red + exit 1 +} + +# --- Validate connection --- +if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) { + Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red + exit 1 +} + +# --- Build arguments as single string --- +# Note: Start-Process without -NoNewWindow uses ShellExecute. +# Passing ArgumentList as array can corrupt Cyrillic when ShellExecute +# re-joins elements. Single string avoids this. +$argString = "ENTERPRISE" + +if ($InfoBaseServer -and $InfoBaseRef) { + $argString += " /S `"$InfoBaseServer/$InfoBaseRef`"" +} else { + $argString += " /F `"$InfoBasePath`"" +} + +if ($UserName) { $argString += " /N`"$UserName`"" } +if ($Password) { $argString += " /P`"$Password`"" } + +# --- Optional params --- +if ($Execute) { + $ext = [System.IO.Path]::GetExtension($Execute).ToLower() + if ($ext -eq ".erf") { + Write-Host "[WARN] /Execute не поддерживает ERF-файлы (внешние отчёты)." -ForegroundColor Yellow + Write-Host " Откройте отчёт через «Файл -> Открыть»: $Execute" -ForegroundColor Yellow + Write-Host " Запускаю базу без /Execute." -ForegroundColor Yellow + $Execute = "" + } +} +if ($Execute) { + $argString += " /Execute `"$Execute`"" +} +if ($CParam) { + $argString += " /C `"$CParam`"" +} +if ($URL) { + $argString += " /URL `"$URL`"" +} + +$argString += " /DisableStartupDialogs" + +# --- Execute (background, no wait) --- +Write-Host "Running: 1cv8.exe $argString" +Start-Process -FilePath $V8Path -ArgumentList $argString +Write-Host "1C:Enterprise launched" -ForegroundColor Green diff --git a/.codex/skills/db-run/scripts/db-run.py b/.codex/skills/db-run/scripts/db-run.py new file mode 100644 index 00000000..b19807a8 --- /dev/null +++ b/.codex/skills/db-run/scripts/db-run.py @@ -0,0 +1,94 @@ +#!/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(): + sys.stdout.reconfigure(encoding="utf-8") + sys.stderr.reconfigure(encoding="utf-8") + 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", 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", execute]) + if args.CParam: + arguments.extend(["/C", args.CParam]) + if args.URL: + arguments.extend(["/URL", 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/.codex/skills/db-update/SKILL.md b/.codex/skills/db-update/SKILL.md new file mode 100644 index 00000000..1bab1b40 --- /dev/null +++ b/.codex/skills/db-update/SKILL.md @@ -0,0 +1,93 @@ +--- +name: db-update +description: Обновление конфигурации базы данных 1С. Используй когда нужно обновить БД, применить конфигурацию, UpdateDBCfg +argument-hint: "[database]" +allowed-tools: + - Bash + - Read + - Glob + - AskUserQuestion +--- + +# /db-update — Обновление конфигурации БД + +Применяет изменения основной конфигурации к конфигурации базы данных (`/UpdateDBCfg`). Обязательный шаг после `/db-load-cf`, `/db-load-xml`, `/db-load-git`. + +## Usage + +``` +/db-update [database] +/db-update dev +/db-update dev -Dynamic+ +``` + +## Параметры подключения + +Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу: +1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую +2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` +3. Если не указал — сопоставь текущую ветку Git с `databases[].branches` +4. Если ветка не совпала — используй `default` +Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1` +Если файла нет — предложи `/db-list add`. +Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. + +## Команда + +```powershell +powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/db-update/scripts/db-update.ps1" <параметры> +``` + +### Параметры скрипта + +| Параметр | Обязательный | Описание | +|----------|:------------:|----------| +| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) | +| `-InfoBasePath <путь>` | * | Файловая база | +| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) | +| `-InfoBaseRef <имя>` | * | Имя базы на сервере | +| `-UserName <имя>` | нет | Имя пользователя | +| `-Password <пароль>` | нет | Пароль | +| `-Extension <имя>` | нет | Обновить расширение | +| `-AllExtensions` | нет | Обновить все расширения | +| `-Dynamic <+/->` | нет | `+` — динамическое обновление, `-` — отключить | +| `-Server` | нет | Обновление на стороне сервера | +| `-WarningsAsErrors` | нет | Предупреждения считать ошибками | + +> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef` + +### Фоновое обновление (серверная база) + +| Параметр | Описание | +|----------|----------| +| `-BackgroundStart` | Начать фоновое обновление | +| `-BackgroundFinish` | Дождаться окончания | +| `-BackgroundCancel` | Отменить | +| `-BackgroundSuspend` | Приостановить | +| `-BackgroundResume` | Возобновить | + +## Коды возврата + +| Код | Описание | +|-----|----------| +| 0 | Успешно | +| 1 | Ошибка (см. лог) | + +## Предупреждения + +- Если обновление **не динамическое** — потребуется **монопольный доступ** к базе (все пользователи должны выйти) +- Для серверных баз рекомендуется `-Dynamic+` для обновления без остановки +- Если структура данных существенно изменилась (удаление реквизитов, изменение типов) — динамическое обновление может быть невозможно + +## Примеры + +```powershell +# Обычное обновление (файловая база) +powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/db-update/scripts/db-update.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" + +# Динамическое обновление (серверная база) +powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/db-update/scripts/db-update.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -Dynamic "+" + +# Обновление расширения +powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/db-update/scripts/db-update.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -Extension "МоёРасширение" +``` diff --git a/.codex/skills/db-update/scripts/db-update.ps1 b/.codex/skills/db-update/scripts/db-update.ps1 new file mode 100644 index 00000000..fb83b73f --- /dev/null +++ b/.codex/skills/db-update/scripts/db-update.ps1 @@ -0,0 +1,184 @@ +# db-update v1.0 — Update 1C database configuration +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +<# +.SYNOPSIS + Обновление конфигурации базы данных 1С + +.DESCRIPTION + Применяет изменения основной конфигурации к конфигурации базы данных. + Поддерживает динамическое обновление, обновление расширений. + +.PARAMETER V8Path + Путь к каталогу bin платформы или к 1cv8.exe + +.PARAMETER InfoBasePath + Путь к файловой информационной базе + +.PARAMETER InfoBaseServer + Сервер 1С (для серверной базы) + +.PARAMETER InfoBaseRef + Имя базы на сервере + +.PARAMETER UserName + Имя пользователя 1С + +.PARAMETER Password + Пароль пользователя + +.PARAMETER Extension + Имя расширения для обновления + +.PARAMETER AllExtensions + Обновить все расширения + +.PARAMETER Dynamic + Динамическое обновление: "+" включить, "-" отключить + +.PARAMETER Server + Обновление на стороне сервера + +.PARAMETER WarningsAsErrors + Предупреждения считать ошибками + +.EXAMPLE + .\db-update.ps1 -InfoBasePath "C:\Bases\MyDB" + +.EXAMPLE + .\db-update.ps1 -InfoBasePath "C:\Bases\MyDB" -Dynamic "+" -Extension "МоёРасширение" +#> + +[CmdletBinding()] +param( + [Parameter(Mandatory=$false)] + [string]$V8Path, + + [Parameter(Mandatory=$false)] + [string]$InfoBasePath, + + [Parameter(Mandatory=$false)] + [string]$InfoBaseServer, + + [Parameter(Mandatory=$false)] + [string]$InfoBaseRef, + + [Parameter(Mandatory=$false)] + [string]$UserName, + + [Parameter(Mandatory=$false)] + [string]$Password, + + [Parameter(Mandatory=$false)] + [string]$Extension, + + [Parameter(Mandatory=$false)] + [switch]$AllExtensions, + + [Parameter(Mandatory=$false)] + [ValidateSet("+", "-")] + [string]$Dynamic, + + [Parameter(Mandatory=$false)] + [switch]$Server, + + [Parameter(Mandatory=$false)] + [switch]$WarningsAsErrors +) + +$OutputEncoding = [System.Text.Encoding]::UTF8 +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- Resolve V8Path --- +if (-not $V8Path) { + $found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1 + if ($found) { + $V8Path = $found.FullName + } else { + Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red + exit 1 + } +} elseif (Test-Path $V8Path -PathType Container) { + $V8Path = Join-Path $V8Path "1cv8.exe" +} + +if (-not (Test-Path $V8Path)) { + Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red + exit 1 +} + +# --- Validate connection --- +if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) { + Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red + exit 1 +} + +# --- Temp dir --- +$tempDir = Join-Path $env:TEMP "db_update_$(Get-Random)" +New-Item -ItemType Directory -Path $tempDir -Force | Out-Null + +try { + # --- Build arguments --- + $arguments = @("DESIGNER") + + if ($InfoBaseServer -and $InfoBaseRef) { + $arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`"" + } else { + $arguments += "/F", "`"$InfoBasePath`"" + } + + if ($UserName) { $arguments += "/N`"$UserName`"" } + if ($Password) { $arguments += "/P`"$Password`"" } + + $arguments += "/UpdateDBCfg" + + # --- Options --- + if ($Dynamic) { + $arguments += "-Dynamic$Dynamic" + } + if ($Server) { + $arguments += "-Server" + } + if ($WarningsAsErrors) { + $arguments += "-WarningsAsErrors" + } + + # --- Extensions --- + if ($Extension) { + $arguments += "-Extension", "`"$Extension`"" + } elseif ($AllExtensions) { + $arguments += "-AllExtensions" + } + + # --- Output --- + $outFile = Join-Path $tempDir "update_log.txt" + $arguments += "/Out", "`"$outFile`"" + $arguments += "/DisableStartupDialogs" + + # --- Execute --- + Write-Host "Running: 1cv8.exe $($arguments -join ' ')" + $process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru + $exitCode = $process.ExitCode + + # --- Result --- + if ($exitCode -eq 0) { + Write-Host "Database configuration updated successfully" -ForegroundColor Green + } else { + Write-Host "Error updating database configuration (code: $exitCode)" -ForegroundColor Red + } + + if (Test-Path $outFile) { + $logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue + if ($logContent) { + Write-Host "--- Log ---" + Write-Host $logContent + Write-Host "--- End ---" + } + } + + exit $exitCode + +} finally { + if (Test-Path $tempDir) { + Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue + } +} diff --git a/.codex/skills/db-update/scripts/db-update.py b/.codex/skills/db-update/scripts/db-update.py new file mode 100644 index 00000000..c6257d6c --- /dev/null +++ b/.codex/skills/db-update/scripts/db-update.py @@ -0,0 +1,133 @@ +#!/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(): + sys.stdout.reconfigure(encoding="utf-8") + sys.stderr.reconfigure(encoding="utf-8") + 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", 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", args.Extension]) + elif args.AllExtensions: + arguments.append("-AllExtensions") + + # --- Output --- + out_file = os.path.join(temp_dir, "update_log.txt") + arguments.extend(["/Out", 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/.codex/skills/epf-bsp-add-command/SKILL.md b/.codex/skills/epf-bsp-add-command/SKILL.md new file mode 100644 index 00000000..f2e0168b --- /dev/null +++ b/.codex/skills/epf-bsp-add-command/SKILL.md @@ -0,0 +1,196 @@ +--- +name: epf-bsp-add-command +description: Определить команду в БСП‑описании обработки (`СведенияОВнешнейОбработке`) — открытие формы, вызов клиентского/серверного метода, заполнение объекта и т.п. Используй когда нужно зарегистрировать команду в дополнительной обработке БСП +argument-hint: <Идентификатор> [ТипКоманды] [Представление] +allowed-tools: + - Read + - Edit + - Glob + - Grep +--- + +# /epf-bsp-add-command — Добавление команды БСП + +Добавляет команду в существующую функцию `СведенияОВнешнейОбработке()` и генерирует соответствующий обработчик. + +Предварительно обработка должна быть инициализирована через `/epf-bsp-init`. + +## Usage + +``` +/epf-bsp-add-command <Идентификатор> [ТипКоманды] [Представление] +``` + +| Параметр | Обязательный | По умолчанию | Описание | +|---------------|:------------:|-----------------------|--------------------------------------------| +| ProcessorName | да | — | Имя обработки | +| Идентификатор | да | — | Внутреннее имя команды (латиница) | +| ТипКоманды | нет | из вида обработки | Тип запуска команды (см. маппинг ниже) | +| Представление | нет | = Идентификатор | Отображаемое имя команды для пользователя | +| SrcDir | нет | `src` | Каталог исходников | + +## Маппинг типов команд + +Пользователь может указать тип в свободной форме: + +| Пользователь пишет | ТипКоманды | +|---------------------------------------|-----------------------------------------------------| +| открыть форму, форма | `ТипКомандыОткрытиеФормы()` | +| клиентский метод, на клиенте | `ТипКомандыВызовКлиентскогоМетода()` | +| серверный метод, на сервере | `ТипКомандыВызовСерверногоМетода()` | +| заполнение формы, заполнить форму | `ТипКомандыЗаполнениеФормы()` | +| сценарий, безопасный режим | `ТипКомандыСценарийВБезопасномРежиме()` | + +Если пользователь не указал тип — определи по виду обработки из существующего кода `СведенияОВнешнейОбработке()`: + +| Вид обработки (из кода) | ТипКоманды по умолчанию | +|----------------------------|-------------------------------------------| +| ДополнительнаяОбработка | `ТипКомандыОткрытиеФормы()` | +| ДополнительныйОтчет | `ТипКомандыОткрытиеФормы()` | +| ЗаполнениеОбъекта | `ТипКомандыВызовСерверногоМетода()` | +| Отчет | `ТипКомандыОткрытиеФормы()` | +| ПечатнаяФорма | `ТипКомандыВызовСерверногоМетода()` | +| СозданиеСвязанныхОбъектов | `ТипКомандыВызовСерверногоМетода()` | + +## Шаблон добавления команды + +Вставляется в `СведенияОВнешнейОбработке()` **перед** строкой `Возврат ПараметрыРегистрации`: + +```bsl + НоваяКоманда = ПараметрыРегистрации.Команды.Добавить(); + НоваяКоманда.Представление = НСтр("ru = '{{Представление}}'"); + НоваяКоманда.Идентификатор = "{{Идентификатор}}"; + НоваяКоманда.Использование = ДополнительныеОтчетыИОбработкиКлиентСервер.{{ТипКоманды}}; + НоваяКоманда.ПоказыватьОповещение = Ложь; +``` + +Для печатных форм (ВидОбработкиПечатнаяФорма) добавь также: + +```bsl + НоваяКоманда.Модификатор = "ПечатьMXL"; +``` + +Примечание: в отличие от первой команды (из `/epf-bsp-init`), дополнительные команды используют строковые литералы `НСтр("ru = '...'")` для представления и строку для идентификатора, а не `Метаданные()`. + +## Шаблоны обработчиков + +### ВызовСерверногоМетода — если обработчик уже есть + +Если процедура `ВыполнитьКоманду` уже существует в модуле объекта, добавь ветку перед `КонецЕсли`: + +```bsl + ИначеЕсли ИдентификаторКоманды = "{{Идентификатор}}" Тогда + // TODO: Реализация {{Идентификатор}} +``` + +### ВызовСерверногоМетода — если обработчика нет + +Для глобальных обработок (без `ОбъектыНазначения`): + +```bsl +Процедура ВыполнитьКоманду(ИдентификаторКоманды, ПараметрыВыполненияКоманды) Экспорт + + Если ИдентификаторКоманды = "{{Идентификатор}}" Тогда + // TODO: Реализация {{Идентификатор}} + КонецЕсли; + +КонецПроцедуры +``` + +Для назначаемых обработок (с `ОбъектыНазначения`): + +```bsl +Процедура ВыполнитьКоманду(ИдентификаторКоманды, ОбъектыНазначения, ПараметрыВыполненияКоманды) Экспорт + + Если ИдентификаторКоманды = "{{Идентификатор}}" Тогда + // TODO: Реализация {{Идентификатор}} + КонецЕсли; + +КонецПроцедуры +``` + +### ПечатнаяФорма — если процедура Печать уже есть + +Добавь блок перед `КонецПроцедуры`: + +```bsl + ПечатнаяФорма = УправлениеПечатью.СведенияОПечатнойФорме(КоллекцияПечатныхФорм, "{{Идентификатор}}"); + Если ПечатнаяФорма <> Неопределено Тогда + ПечатнаяФорма.ТабличныйДокумент = Сформировать{{Идентификатор}}(МассивОбъектов, ОбъектыПечати); + ПечатнаяФорма.СинонимМакета = НСтр("ru = '{{Представление}}'"); + КонецЕсли; +``` + +### ПечатнаяФорма — если процедуры Печать нет + +```bsl +Процедура Печать(МассивОбъектов, КоллекцияПечатныхФорм, ОбъектыПечати, ПараметрыВывода) Экспорт + + ПечатнаяФорма = УправлениеПечатью.СведенияОПечатнойФорме(КоллекцияПечатныхФорм, "{{Идентификатор}}"); + Если ПечатнаяФорма <> Неопределено Тогда + ПечатнаяФорма.ТабличныйДокумент = Сформировать{{Идентификатор}}(МассивОбъектов, ОбъектыПечати); + ПечатнаяФорма.СинонимМакета = НСтр("ru = '{{Представление}}'"); + КонецЕсли; + +КонецПроцедуры +``` + +### ВызовКлиентскогоМетода + +Добавляется в **модуль формы** (`Forms//Ext/Form/Module.bsl`): + +Для глобальных обработок: + +```bsl +&НаКлиенте +Процедура ВыполнитьКоманду(ИдентификаторКоманды) Экспорт + + Если ИдентификаторКоманды = "{{Идентификатор}}" Тогда + // TODO: Реализация {{Идентификатор}} + КонецЕсли; + +КонецПроцедуры +``` + +Для назначаемых обработок: + +```bsl +&НаКлиенте +Процедура ВыполнитьКоманду(ИдентификаторКоманды, ОбъектыНазначенияМассив) Экспорт + + Если ИдентификаторКоманды = "{{Идентификатор}}" Тогда + // TODO: Реализация {{Идентификатор}} + КонецЕсли; + +КонецПроцедуры +``` + +Если процедура уже есть — добавь ветку `ИначеЕсли`. + +## Инструкции + +1. Найди и прочитай `ObjectModule.bsl` через Glob: `src/{{ProcessorName}}/Ext/ObjectModule.bsl` +2. Убедись что `СведенияОВнешнейОбработке()` существует. Если нет — предложи вызвать `/epf-bsp-init` +3. Определи вид обработки из существующего кода (найди строку с `ВидОбработки...()`) +4. Вставь блок команды **перед** `Возврат ПараметрыРегистрации` +5. Добавь обработчик: + - Для серверных обработчиков — в `ObjectModule.bsl`, область `ПрограммныйИнтерфейс` + - Для клиентских обработчиков — в модуль формы (найти через Glob: `src/{{ProcessorName}}/Forms/*/Ext/Form/Module.bsl`) +6. Если обработчик (`ВыполнитьКоманду` / `Печать`) уже есть — добавь ветку, не создавай дубль процедуры +7. Используй табы для отступов + +## Пример + +Пользователь: `/epf-bsp-add-command МояОбработка ЗаказПокупателя серверный "Заказ покупателя"` + +В `СведенияОВнешнейОбработке()` перед `Возврат` добавится: + +```bsl + НоваяКоманда = ПараметрыРегистрации.Команды.Добавить(); + НоваяКоманда.Представление = НСтр("ru = 'Заказ покупателя'"); + НоваяКоманда.Идентификатор = "ЗаказПокупателя"; + НоваяКоманда.Использование = ДополнительныеОтчетыИОбработкиКлиентСервер.ТипКомандыВызовСерверногоМетода(); + НоваяКоманда.ПоказыватьОповещение = Ложь; +``` + +И в существующую процедуру `ВыполнитьКоманду` добавится блок обработки. diff --git a/.codex/skills/epf-bsp-init/SKILL.md b/.codex/skills/epf-bsp-init/SKILL.md new file mode 100644 index 00000000..a63bf6b6 --- /dev/null +++ b/.codex/skills/epf-bsp-init/SKILL.md @@ -0,0 +1,208 @@ +--- +name: epf-bsp-init +description: Сформировать функцию `СведенияОВнешнейОбработке` в модуле объекта обработки — описание для подключения через подсистему БСП «Дополнительные отчёты и обработки». Используй когда нужно сделать обработку совместимой с БСП, подключаемой через «Дополнительные отчёты и обработки» +argument-hint: <Вид> +allowed-tools: + - Read + - Edit + - Glob + - Grep +--- + +# /epf-bsp-init — Регистрация обработки в БСП + +Добавляет в модуль объекта обработки функцию `СведенияОВнешнейОбработке()`, необходимую для регистрации в подсистеме «Дополнительные отчёты и обработки» БСП. + +## Usage + +``` +/epf-bsp-init <Вид> [Назначение...] +``` + +| Параметр | Обязательный | По умолчанию | Описание | +|---------------|:------------:|--------------|---------------------------------------------------------| +| ProcessorName | да | — | Имя обработки (должна быть создана через `/epf-init`) | +| Вид | да | — | Вид обработки (см. маппинг ниже) | +| Назначение | * | — | Объекты метаданных для назначаемых видов | +| SrcDir | нет | `src` | Каталог исходников | + +\* Назначение обязательно для видов: ЗаполнениеОбъекта, Отчет, ПечатнаяФорма, СозданиеСвязанныхОбъектов. + +## Маппинг вида обработки + +Пользователь может указать вид в свободной форме. Определи нужный по контексту: + +| Пользователь пишет | Вид | API-метод | +|-------------------------------------------|----------------------------|----------------------------------------------| +| доп обработка, обработка, глобальная | ДополнительнаяОбработка | `ВидОбработкиДополнительнаяОбработка()` | +| доп отчёт, глобальный отчёт | ДополнительныйОтчет | `ВидОбработкиДополнительныйОтчет()` | +| заполнение, заполнить | ЗаполнениеОбъекта | `ВидОбработкиЗаполнениеОбъекта()` | +| отчёт (назначаемый, для объекта) | Отчет | `ВидОбработкиОтчет()` | +| печатная форма, печать | ПечатнаяФорма | `ВидОбработкиПечатнаяФорма()` | +| создание связанных объектов | СозданиеСвязанныхОбъектов | `ВидОбработкиСозданиеСвязанныхОбъектов()` | + +## Тип команды по умолчанию + +| Вид | ТипКоманды по умолчанию | +|----------------------------|-------------------------------------------| +| ДополнительнаяОбработка | `ТипКомандыОткрытиеФормы()` | +| ДополнительныйОтчет | `ТипКомандыОткрытиеФормы()` | +| ЗаполнениеОбъекта | `ТипКомандыВызовСерверногоМетода()` | +| Отчет | `ТипКомандыОткрытиеФормы()` | +| ПечатнаяФорма | `ТипКомандыВызовСерверногоМетода()` | +| СозданиеСвязанныхОбъектов | `ТипКомандыВызовСерверногоМетода()` | + +## Шаблон: СведенияОВнешнейОбработке + +Базовый шаблон — одинаковый для всех видов, отличаются только вызовы API-методов и условные секции. + +```bsl +Функция СведенияОВнешнейОбработке() Экспорт + + МетаданныеОбработки = Метаданные(); + + ПараметрыРегистрации = ДополнительныеОтчетыИОбработки.СведенияОВнешнейОбработке("2.2.2.1"); + ПараметрыРегистрации.Вид = ДополнительныеОтчетыИОбработкиКлиентСервер.{{ВидОбработки}}; + ПараметрыРегистрации.Версия = "1.0"; + + {{СЕКЦИЯ_НАЗНАЧЕНИЕ}} + + НоваяКоманда = ПараметрыРегистрации.Команды.Добавить(); + НоваяКоманда.Представление = МетаданныеОбработки.Представление(); + НоваяКоманда.Идентификатор = МетаданныеОбработки.Имя; + НоваяКоманда.Использование = ДополнительныеОтчетыИОбработкиКлиентСервер.{{ТипКоманды}}; + НоваяКоманда.ПоказыватьОповещение = Ложь; + {{СЕКЦИЯ_МОДИФИКАТОР}} + + Возврат ПараметрыРегистрации; + +КонецФункции +``` + +### Подстановки + +- `{{ВидОбработки}}` — API-метод из таблицы маппинга вида +- `{{ТипКоманды}}` — API-метод из таблицы типа команды по умолчанию + +### Условные секции + +**`{{СЕКЦИЯ_НАЗНАЧЕНИЕ}}`** — только для назначаемых видов (ЗаполнениеОбъекта, Отчет, ПечатнаяФорма, СозданиеСвязанныхОбъектов). Одна строка на каждый объект: + +```bsl + ПараметрыРегистрации.Назначение.Добавить("Документ.СчетНаОплату"); +``` + +Формат имени объекта: `ИмяКлассаОбъектаМетаданного.ИмяОбъекта` (например `Документ.СчетНаОплату`, `Справочник.Контрагенты`). + +Для глобальных видов (ДополнительнаяОбработка, ДополнительныйОтчет) — секция не нужна, удалить вместе с пустой строкой. + +**`{{СЕКЦИЯ_МОДИФИКАТОР}}`** — только для ПечатнаяФорма: + +```bsl + НоваяКоманда.Модификатор = "ПечатьMXL"; +``` + +Для остальных видов — удалить вместе с пустой строкой. + +## Шаблоны серверных обработчиков + +Для видов с типом команды `ВызовСерверногоМетода` добавь соответствующую процедуру-обработчик в ту же область `ПрограммныйИнтерфейс`, после `СведенияОВнешнейОбработке`. + +### Для ЗаполнениеОбъекта / СозданиеСвязанныхОбъектов + +```bsl +Процедура ВыполнитьКоманду(ИдентификаторКоманды, ОбъектыНазначения, ПараметрыВыполненияКоманды) Экспорт + + // TODO: Реализация + +КонецПроцедуры +``` + +### Для ПечатнаяФорма + +```bsl +Процедура Печать(МассивОбъектов, КоллекцияПечатныхФорм, ОбъектыПечати, ПараметрыВывода) Экспорт + + // TODO: Реализация + +КонецПроцедуры +``` + +### Для ДополнительнаяОбработка / ДополнительныйОтчет (с ВызовСерверногоМетода) + +Если пользователь явно выбрал серверный метод вместо открытия формы: + +```bsl +Процедура ВыполнитьКоманду(ИдентификаторКоманды, ПараметрыВыполненияКоманды) Экспорт + + // TODO: Реализация + +КонецПроцедуры +``` + +Обрати внимание: у глобальных обработок нет параметра `ОбъектыНазначения`. + +## Инструкции + +1. Найди `ObjectModule.bsl` через Glob: `src/{{ProcessorName}}/Ext/ObjectModule.bsl` +2. Прочитай файл +3. Если `СведенияОВнешнейОбработке` уже есть — сообщи пользователю и не дублируй +4. Если файл не найден — предложи сначала вызвать `/epf-init` +5. Найди область `#Область ПрограммныйИнтерфейс` ... `#КонецОбласти` +6. Вставь функцию `СведенияОВнешнейОбработке()` внутрь этой области +7. Если вид требует серверный обработчик — вставь его тоже в эту область, после функции +8. Используй табы для отступов (как в исходном файле) + +## Пример + +Пользователь: `/epf-bsp-init МояОбработка печатная форма для Документ.СчетНаОплату` + +Результат в `ObjectModule.bsl`: + +```bsl +#Область ОписаниеПеременных + +#КонецОбласти + +#Область ПрограммныйИнтерфейс + +Функция СведенияОВнешнейОбработке() Экспорт + + МетаданныеОбработки = Метаданные(); + + ПараметрыРегистрации = ДополнительныеОтчетыИОбработки.СведенияОВнешнейОбработке("2.2.2.1"); + ПараметрыРегистрации.Вид = ДополнительныеОтчетыИОбработкиКлиентСервер.ВидОбработкиПечатнаяФорма(); + ПараметрыРегистрации.Версия = "1.0"; + + ПараметрыРегистрации.Назначение.Добавить("Документ.СчетНаОплату"); + + НоваяКоманда = ПараметрыРегистрации.Команды.Добавить(); + НоваяКоманда.Представление = МетаданныеОбработки.Представление(); + НоваяКоманда.Идентификатор = МетаданныеОбработки.Имя; + НоваяКоманда.Использование = ДополнительныеОтчетыИОбработкиКлиентСервер.ТипКомандыВызовСерверногоМетода(); + НоваяКоманда.ПоказыватьОповещение = Ложь; + НоваяКоманда.Модификатор = "ПечатьMXL"; + + Возврат ПараметрыРегистрации; + +КонецФункции + +Процедура Печать(МассивОбъектов, КоллекцияПечатныхФорм, ОбъектыПечати, ПараметрыВывода) Экспорт + + // TODO: Реализация + +КонецПроцедуры + +#КонецОбласти + +#Область СлужебныеПроцедурыИФункции + +#КонецОбласти +``` + +## Дальнейшие шаги + +- Добавить ещё команду: `/epf-bsp-add-command` +- Добавить форму: `/form-add` +- Добавить макет: `/template-add` +- Собрать EPF: `/epf-build` diff --git a/.codex/skills/epf-build/SKILL.md b/.codex/skills/epf-build/SKILL.md new file mode 100644 index 00000000..32bf06ea --- /dev/null +++ b/.codex/skills/epf-build/SKILL.md @@ -0,0 +1,69 @@ +--- +name: epf-build +description: Собрать внешнюю обработку 1С (EPF/ERF) из XML-исходников. Используй когда пользователь просит собрать, скомпилировать обработку или получить EPF/ERF файл из исходников +argument-hint: +allowed-tools: + - Bash + - Read + - Glob + - Grep +--- + +# /epf-build — Сборка обработки + +## Usage + +``` +/epf-build [SrcDir] [OutDir] +``` + +| Параметр | Обязательный | По умолчанию | Описание | +|---------------|:------------:|--------------|--------------------------------------| +| ProcessorName | да | — | Имя обработки (имя корневого XML) | +| SrcDir | нет | `src` | Каталог исходников | +| OutDir | нет | `build` | Каталог для результата | + +## Параметры подключения (опционально) + +Предпочтительно использовать конкретную базу — это надёжнее и не требует создания временной базы. + +1. Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` и разреши базу: +2. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую +3. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` +4. Если не указал — сопоставь текущую ветку Git с `databases[].branches` +5. Если ветка не совпала — используй `default` +6. Если `.v8-project.json` нет или база не найдена — не указывай параметры подключения: скрипт автоматически создаст временную базу. Для EPF со ссылочными типами (CatalogRef, DocumentRef и т.д.) генерируются заглушки метаданных. Временная база удаляется после сборки. + +Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1` +Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. + +## Команда + +```powershell +powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/epf-build/scripts/epf-build.ps1" <параметры> +``` + +### Параметры скрипта + +| Параметр | Обязательный | Описание | +|----------|:------------:|----------| +| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) | +| `-InfoBasePath <путь>` | * | Файловая база | +| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) | +| `-InfoBaseRef <имя>` | * | Имя базы на сервере | +| `-UserName <имя>` | нет | Имя пользователя | +| `-Password <пароль>` | нет | Пароль | +| `-SourceFile <путь>` | да | Путь к корневому XML-файлу исходников | +| `-OutputFile <путь>` | да | Путь к выходному EPF/ERF-файлу | + +> `*` — опционально. Если не указано — автоматически создаётся временная база со заглушками метаданных + +## Примеры + +```powershell +# Сборка обработки (файловая база) +powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/epf-build/scripts/epf-build.ps1" -InfoBasePath "C:\Bases\MyDB" -SourceFile "src/МояОбработка.xml" -OutputFile "build/МояОбработка.epf" + +# Серверная база +powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/epf-build/scripts/epf-build.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -SourceFile "src/МояОбработка.xml" -OutputFile "build/МояОбработка.epf" +``` diff --git a/.codex/skills/epf-build/scripts/epf-build.ps1 b/.codex/skills/epf-build/scripts/epf-build.ps1 new file mode 100644 index 00000000..d099615d --- /dev/null +++ b/.codex/skills/epf-build/scripts/epf-build.ps1 @@ -0,0 +1,173 @@ +# epf-build v1.0 — Build external data processor or report (EPF/ERF) from XML sources +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +<# +.SYNOPSIS + Сборка внешней обработки/отчёта 1С из XML-исходников + +.DESCRIPTION + Собирает EPF/ERF-файл из XML-исходников с помощью платформы 1С. + Общий скрипт для epf-build и erf-build. + +.PARAMETER V8Path + Путь к каталогу bin платформы или к 1cv8.exe + +.PARAMETER InfoBasePath + Путь к файловой информационной базе + +.PARAMETER InfoBaseServer + Сервер 1С (для серверной базы) + +.PARAMETER InfoBaseRef + Имя базы на сервере + +.PARAMETER UserName + Имя пользователя 1С + +.PARAMETER Password + Пароль пользователя + +.PARAMETER SourceFile + Путь к корневому XML-файлу исходников + +.PARAMETER OutputFile + Путь к выходному EPF/ERF-файлу + +.EXAMPLE + .\epf-build.ps1 -InfoBasePath "C:\Bases\MyDB" -SourceFile "src\МояОбработка.xml" -OutputFile "build\МояОбработка.epf" + +.EXAMPLE + .\epf-build.ps1 -InfoBasePath "C:\Bases\MyDB" -SourceFile "src\МойОтчёт.xml" -OutputFile "build\МойОтчёт.erf" +#> + +[CmdletBinding()] +param( + [Parameter(Mandatory=$false)] + [string]$V8Path, + + [Parameter(Mandatory=$false)] + [string]$InfoBasePath, + + [Parameter(Mandatory=$false)] + [string]$InfoBaseServer, + + [Parameter(Mandatory=$false)] + [string]$InfoBaseRef, + + [Parameter(Mandatory=$false)] + [string]$UserName, + + [Parameter(Mandatory=$false)] + [string]$Password, + + [Parameter(Mandatory=$true)] + [string]$SourceFile, + + [Parameter(Mandatory=$true)] + [string]$OutputFile +) + +$OutputEncoding = [System.Text.Encoding]::UTF8 +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- Resolve V8Path --- +if (-not $V8Path) { + $found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1 + if ($found) { + $V8Path = $found.FullName + } else { + Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red + exit 1 + } +} elseif (Test-Path $V8Path -PathType Container) { + $V8Path = Join-Path $V8Path "1cv8.exe" +} + +if (-not (Test-Path $V8Path)) { + Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red + exit 1 +} + +# --- Auto-create stub database if no connection specified --- +$autoCreatedBase = $null +if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) { + $sourceDir = Split-Path $SourceFile -Parent + $autoBasePath = Join-Path $env:TEMP "epf_stub_db_$(Get-Random)" + $stubScript = Join-Path $PSScriptRoot "stub-db-create.ps1" + Write-Host "No database specified. Creating temporary stub database..." + $stubArgs = "-SourceDir `"$sourceDir`" -V8Path `"$V8Path`" -TempBasePath `"$autoBasePath`"" + $stubProc = Start-Process -FilePath "powershell.exe" -ArgumentList "-NoProfile -File `"$stubScript`" $stubArgs" -NoNewWindow -Wait -PassThru + if ($stubProc.ExitCode -ne 0) { + Write-Host "Error: failed to create stub database" -ForegroundColor Red + exit 1 + } + $InfoBasePath = $autoBasePath + $autoCreatedBase = $autoBasePath +} + +# --- Validate source file --- +if (-not (Test-Path $SourceFile)) { + Write-Host "Error: source file not found: $SourceFile" -ForegroundColor Red + exit 1 +} + +# --- Ensure output directory exists --- +$outDir = Split-Path $OutputFile -Parent +if ($outDir -and -not (Test-Path $outDir)) { + New-Item -ItemType Directory -Path $outDir -Force | Out-Null +} + +# --- Temp dir --- +$tempDir = Join-Path $env:TEMP "epf_build_$(Get-Random)" +New-Item -ItemType Directory -Path $tempDir -Force | Out-Null + +try { + # --- Build arguments --- + $arguments = @("DESIGNER") + + if ($InfoBaseServer -and $InfoBaseRef) { + $arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`"" + } else { + $arguments += "/F", "`"$InfoBasePath`"" + } + + if ($UserName) { $arguments += "/N`"$UserName`"" } + if ($Password) { $arguments += "/P`"$Password`"" } + + $arguments += "/LoadExternalDataProcessorOrReportFromFiles", "`"$SourceFile`"", "`"$OutputFile`"" + + # --- Output --- + $outFile = Join-Path $tempDir "build_log.txt" + $arguments += "/Out", "`"$outFile`"" + $arguments += "/DisableStartupDialogs" + + # --- Execute --- + Write-Host "Running: 1cv8.exe $($arguments -join ' ')" + $process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru + $exitCode = $process.ExitCode + + # --- Result --- + if ($exitCode -eq 0) { + Write-Host "Build completed successfully: $OutputFile" -ForegroundColor Green + } else { + Write-Host "Error building (code: $exitCode)" -ForegroundColor Red + } + + if (Test-Path $outFile) { + $logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue + if ($logContent) { + Write-Host "--- Log ---" + Write-Host $logContent + Write-Host "--- End ---" + } + } + + exit $exitCode + +} finally { + if (Test-Path $tempDir) { + Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue + } + if ($autoCreatedBase -and (Test-Path $autoCreatedBase)) { + Remove-Item -Path $autoCreatedBase -Recurse -Force -ErrorAction SilentlyContinue + } +} diff --git a/.codex/skills/epf-build/scripts/epf-build.py b/.codex/skills/epf-build/scripts/epf-build.py new file mode 100644 index 00000000..c4ff7d11 --- /dev/null +++ b/.codex/skills/epf-build/scripts/epf-build.py @@ -0,0 +1,143 @@ +#!/usr/bin/env python3 +# epf-build v1.0 — Build external data processor or report (EPF/ERF) from XML sources +# 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(): + sys.stdout.reconfigure(encoding="utf-8") + sys.stderr.reconfigure(encoding="utf-8") + parser = argparse.ArgumentParser( + description="Build external data processor or report (EPF/ERF) from XML sources", + 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("-SourceFile", required=True, help="Path to root XML source file") + parser.add_argument("-OutputFile", required=True, help="Path to output EPF/ERF file") + args = parser.parse_args() + + # --- Resolve V8Path --- + v8path = resolve_v8path(args.V8Path) + + # --- Auto-create stub database if no connection specified --- + auto_created_base = None + if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef): + source_dir = os.path.dirname(os.path.abspath(args.SourceFile)) + auto_base_path = os.path.join(tempfile.gettempdir(), f"epf_stub_db_{random.randint(0, 999999)}") + stub_script = os.path.join(os.path.dirname(os.path.abspath(__file__)), "stub-db-create.py") + print("No database specified. Creating temporary stub database...") + result = subprocess.run( + [sys.executable, stub_script, "-SourceDir", source_dir, "-V8Path", v8path, "-TempBasePath", auto_base_path], + capture_output=False, + ) + if result.returncode != 0: + print("Error: failed to create stub database", file=sys.stderr) + sys.exit(1) + args.InfoBasePath = auto_base_path + auto_created_base = auto_base_path + + # --- Validate source file --- + if not os.path.isfile(args.SourceFile): + print(f"Error: source file not found: {args.SourceFile}", file=sys.stderr) + sys.exit(1) + + # --- Ensure output directory exists --- + out_dir = os.path.dirname(args.OutputFile) + if out_dir and not os.path.exists(out_dir): + os.makedirs(out_dir, exist_ok=True) + + # --- Temp dir --- + temp_dir = os.path.join(tempfile.gettempdir(), f"epf_build_{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", args.InfoBasePath] + + if args.UserName: + arguments.append(f"/N{args.UserName}") + if args.Password: + arguments.append(f"/P{args.Password}") + + arguments += ["/LoadExternalDataProcessorOrReportFromFiles", args.SourceFile, args.OutputFile] + + # --- Output --- + out_file = os.path.join(temp_dir, "build_log.txt") + arguments += ["/Out", 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"Build completed successfully: {args.OutputFile}") + else: + print(f"Error building (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 auto_created_base and os.path.exists(auto_created_base): + shutil.rmtree(auto_created_base, ignore_errors=True) + + +if __name__ == "__main__": + main() diff --git a/.codex/skills/epf-build/scripts/stub-db-create.ps1 b/.codex/skills/epf-build/scripts/stub-db-create.ps1 new file mode 100644 index 00000000..dde1a7bf --- /dev/null +++ b/.codex/skills/epf-build/scripts/stub-db-create.ps1 @@ -0,0 +1,1295 @@ +# stub-db-create v1.0 — Create temp 1C infobase with metadata stubs for EPF/ERF build +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( + [Parameter(Mandatory)] + [string]$SourceDir, + + [Parameter(Mandatory)] + [string]$V8Path, + + [string]$TempBasePath +) + +$ErrorActionPreference = "Stop" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- 1. Scan XML files for reference types --- + +$typeMap = @{} # MetadataType -> @(Name1, Name2, ...) + +$xmlFiles = Get-ChildItem -Path $SourceDir -Filter "*.xml" -Recurse -File +foreach ($f in $xmlFiles) { + $content = [System.IO.File]::ReadAllText($f.FullName, [System.Text.Encoding]::UTF8) + + # Ref types: cfg:CatalogRef.XXX or d5p1:CatalogRef.XXX (and similar depth prefixes d4p1, d3p1, etc.) + $refPattern = '(?:cfg:|d\dp1:)(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|ExchangePlanRef|BusinessProcessRef|TaskRef)\.([A-Za-z\u0400-\u04FF\d_]+)' + foreach ($m in [regex]::Matches($content, $refPattern)) { + $prefix = $m.Groups[1].Value + $name = $m.Groups[2].Value + $metaType = switch ($prefix) { + "CatalogRef" { "Catalog" } + "DocumentRef" { "Document" } + "EnumRef" { "Enum" } + "ChartOfAccountsRef" { "ChartOfAccounts" } + "ChartOfCharacteristicTypesRef" { "ChartOfCharacteristicTypes" } + "ChartOfCalculationTypesRef" { "ChartOfCalculationTypes" } + "ExchangePlanRef" { "ExchangePlan" } + "BusinessProcessRef" { "BusinessProcess" } + "TaskRef" { "Task" } + } + if (-not $typeMap.ContainsKey($metaType)) { $typeMap[$metaType] = @{} } + $typeMap[$metaType][$name] = $true + } + + # Object types: cfg:CatalogObject.XXX etc. + $objPattern = '(?:cfg:|d\dp1:)(CatalogObject|DocumentObject|ChartOfAccountsObject|ChartOfCharacteristicTypesObject|ChartOfCalculationTypesObject|ExchangePlanObject|BusinessProcessObject|TaskObject)\.([A-Za-z\u0400-\u04FF\d_]+)' + foreach ($m in [regex]::Matches($content, $objPattern)) { + $prefix = $m.Groups[1].Value + $name = $m.Groups[2].Value + $metaType = switch ($prefix) { + "CatalogObject" { "Catalog" } + "DocumentObject" { "Document" } + "ChartOfAccountsObject" { "ChartOfAccounts" } + "ChartOfCharacteristicTypesObject" { "ChartOfCharacteristicTypes" } + "ChartOfCalculationTypesObject" { "ChartOfCalculationTypes" } + "ExchangePlanObject" { "ExchangePlan" } + "BusinessProcessObject" { "BusinessProcess" } + "TaskObject" { "Task" } + } + if (-not $typeMap.ContainsKey($metaType)) { $typeMap[$metaType] = @{} } + $typeMap[$metaType][$name] = $true + } + + # RecordSet types: cfg:InformationRegisterRecordSet.XXX etc. + $rsPattern = '(?:cfg:|d\dp1:)(InformationRegisterRecordSet|AccumulationRegisterRecordSet|AccountingRegisterRecordSet|CalculationRegisterRecordSet)\.([A-Za-z\u0400-\u04FF\d_]+)' + foreach ($m in [regex]::Matches($content, $rsPattern)) { + $prefix = $m.Groups[1].Value + $name = $m.Groups[2].Value + $metaType = switch ($prefix) { + "InformationRegisterRecordSet" { "InformationRegister" } + "AccumulationRegisterRecordSet" { "AccumulationRegister" } + "AccountingRegisterRecordSet" { "AccountingRegister" } + "CalculationRegisterRecordSet" { "CalculationRegister" } + } + if (-not $typeMap.ContainsKey($metaType)) { $typeMap[$metaType] = @{} } + $typeMap[$metaType][$name] = $true + } + + # Characteristic TypeSet: cfg:Characteristic.XXX + $charPattern = 'cfg:Characteristic\.([A-Za-z\u0400-\u04FF\d_]+)' + foreach ($m in [regex]::Matches($content, $charPattern)) { + $name = $m.Groups[1].Value + if (-not $typeMap.ContainsKey("ChartOfCharacteristicTypes")) { $typeMap["ChartOfCharacteristicTypes"] = @{} } + $typeMap["ChartOfCharacteristicTypes"][$name] = $true + } + + # DefinedType TypeSet: cfg:DefinedType.XXX + $dtPattern = 'cfg:DefinedType\.([A-Za-z\u0400-\u04FF\d_]+)' + foreach ($m in [regex]::Matches($content, $dtPattern)) { + $name = $m.Groups[1].Value + if (-not $typeMap.ContainsKey("DefinedType")) { $typeMap["DefinedType"] = @{} } + $typeMap["DefinedType"][$name] = $true + } +} + +# --- 1b. Scan Form.xml for register record set columns --- +# When a form attribute has type like InformationRegisterRecordSet.XXX, +# the form references columns via DataPath "AttrName.ColumnName". +# We need to create matching dimensions/resources/attributes in stub registers. + +$registerColumns = @{} # "RegisterType.RegisterName" -> @{ col1=$true; col2=$true } + +# Standard attributes that don't need explicit declaration +$stdRegCols = @("LineNumber","Period","Recorder","Active","RecordType") + +foreach ($f in $xmlFiles) { + $content = [System.IO.File]::ReadAllText($f.FullName, [System.Text.Encoding]::UTF8) + + # Find form attributes with register record set types using XmlDocument for reliability + $regAttrMap = @{} # formAttrName -> "RegisterType.RegisterName" + + # Only process Form.xml files (they contain with children) + if ($f.Name -eq "Form.xml" -and $content -match '') { + try { + $xml = New-Object System.Xml.XmlDocument + $xml.LoadXml($content) + $nsMgr = New-Object System.Xml.XmlNamespaceManager($xml.NameTable) + $nsMgr.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core") + $nsMgr.AddNamespace("f", "http://v8.1c.ru/8.3/xcf/logform") + $attrNodes = $xml.SelectNodes("//f:Attributes/f:Attribute", $nsMgr) + foreach ($attrNode in $attrNodes) { + $attrName = $attrNode.GetAttribute("name") + $typeNodes = $attrNode.SelectNodes("f:Type/v8:Type", $nsMgr) + foreach ($tn in $typeNodes) { + $typeText = $tn.InnerText + $rsMatch = [regex]::Match($typeText, '^(?:cfg:|d\dp1:)(InformationRegisterRecordSet|AccumulationRegisterRecordSet|AccountingRegisterRecordSet|CalculationRegisterRecordSet)\.(.+)$') + if ($rsMatch.Success) { + $rsPrefix = $rsMatch.Groups[1].Value + $regName = $rsMatch.Groups[2].Value + $regType = switch ($rsPrefix) { + "InformationRegisterRecordSet" { "InformationRegister" } + "AccumulationRegisterRecordSet" { "AccumulationRegister" } + "AccountingRegisterRecordSet" { "AccountingRegister" } + "CalculationRegisterRecordSet" { "CalculationRegister" } + } + $regKey = "$regType.$regName" + $regAttrMap[$attrName] = $regKey + if (-not $registerColumns.ContainsKey($regKey)) { + $registerColumns[$regKey] = @{} + } + } + } + } + } catch { + # XML parse failed, skip + } + } + + # Now find DataPath references like "AttrName.ColumnName" + if ($regAttrMap.Count -gt 0) { + $dpPattern = '([A-Za-z\u0400-\u04FF\d_]+)\.([A-Za-z\u0400-\u04FF\d_]+)' + foreach ($m in [regex]::Matches($content, $dpPattern)) { + $attrName = $m.Groups[1].Value + $colName = $m.Groups[2].Value + if ($regAttrMap.ContainsKey($attrName) -and $colName -notin $stdRegCols) { + $regKey = $regAttrMap[$attrName] + $registerColumns[$regKey][$colName] = $true + } + } + } +} + +$hasRefTypes = $typeMap.Count -gt 0 + +# --- 2. Determine TempBasePath --- +if (-not $TempBasePath) { + $TempBasePath = Join-Path $env:TEMP "epf_stub_db_$(Get-Random)" +} + +# --- 3. If registers need a registrator, add stub document --- +$registratorTypes = @("AccumulationRegister","AccountingRegister","CalculationRegister") +$needsRegistrator = $false +foreach ($rt in $registratorTypes) { + if ($typeMap.ContainsKey($rt) -and $typeMap[$rt].Count -gt 0) { + $needsRegistrator = $true + break + } +} +if ($needsRegistrator) { + if (-not $typeMap.ContainsKey("Document")) { $typeMap["Document"] = @{} } + $typeMap["Document"]["ЗаглушкаРегистратора"] = $true +} + +# --- 4. Generate configuration XML --- + +if ($hasRefTypes) { + $enc = New-Object System.Text.UTF8Encoding($true) + $cfgDir = Join-Path $TempBasePath "cfg" + New-Item -ItemType Directory -Path $cfgDir -Force | Out-Null + + $ns = '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" version="2.17"' + + # GeneratedType definitions per metadata type + $gtDefs = @{ + "Catalog" = @( + @{p="CatalogObject";c="Object"},@{p="CatalogRef";c="Ref"},@{p="CatalogSelection";c="Selection"}, + @{p="CatalogList";c="List"},@{p="CatalogManager";c="Manager"} + ) + "Document" = @( + @{p="DocumentObject";c="Object"},@{p="DocumentRef";c="Ref"},@{p="DocumentSelection";c="Selection"}, + @{p="DocumentList";c="List"},@{p="DocumentManager";c="Manager"} + ) + "Enum" = @( + @{p="EnumRef";c="Ref"},@{p="EnumManager";c="Manager"},@{p="EnumList";c="List"} + ) + "ChartOfAccounts" = @( + @{p="ChartOfAccountsObject";c="Object"},@{p="ChartOfAccountsRef";c="Ref"},@{p="ChartOfAccountsSelection";c="Selection"}, + @{p="ChartOfAccountsList";c="List"},@{p="ChartOfAccountsManager";c="Manager"} + ) + "ChartOfCharacteristicTypes" = @( + @{p="ChartOfCharacteristicTypesObject";c="Object"},@{p="ChartOfCharacteristicTypesRef";c="Ref"},@{p="ChartOfCharacteristicTypesSelection";c="Selection"}, + @{p="ChartOfCharacteristicTypesList";c="List"},@{p="Characteristic";c="Characteristic"},@{p="ChartOfCharacteristicTypesManager";c="Manager"} + ) + "ChartOfCalculationTypes" = @( + @{p="ChartOfCalculationTypesObject";c="Object"},@{p="ChartOfCalculationTypesRef";c="Ref"},@{p="ChartOfCalculationTypesSelection";c="Selection"}, + @{p="ChartOfCalculationTypesList";c="List"},@{p="ChartOfCalculationTypesManager";c="Manager"} + ) + "ExchangePlan" = @( + @{p="ExchangePlanObject";c="Object"},@{p="ExchangePlanRef";c="Ref"},@{p="ExchangePlanSelection";c="Selection"}, + @{p="ExchangePlanList";c="List"},@{p="ExchangePlanManager";c="Manager"} + ) + "BusinessProcess" = @( + @{p="BusinessProcessObject";c="Object"},@{p="BusinessProcessRef";c="Ref"},@{p="BusinessProcessSelection";c="Selection"}, + @{p="BusinessProcessList";c="List"},@{p="BusinessProcessManager";c="Manager"} + ) + "Task" = @( + @{p="TaskObject";c="Object"},@{p="TaskRef";c="Ref"},@{p="TaskSelection";c="Selection"}, + @{p="TaskList";c="List"},@{p="TaskManager";c="Manager"} + ) + "InformationRegister" = @( + @{p="InformationRegisterRecord";c="Record"},@{p="InformationRegisterManager";c="Manager"}, + @{p="InformationRegisterSelection";c="Selection"},@{p="InformationRegisterList";c="List"}, + @{p="InformationRegisterRecordSet";c="RecordSet"},@{p="InformationRegisterRecordKey";c="RecordKey"}, + @{p="InformationRegisterRecordManager";c="RecordManager"} + ) + "AccumulationRegister" = @( + @{p="AccumulationRegisterRecord";c="Record"},@{p="AccumulationRegisterManager";c="Manager"}, + @{p="AccumulationRegisterSelection";c="Selection"},@{p="AccumulationRegisterList";c="List"}, + @{p="AccumulationRegisterRecordSet";c="RecordSet"},@{p="AccumulationRegisterRecordKey";c="RecordKey"} + ) + "AccountingRegister" = @( + @{p="AccountingRegisterRecord";c="Record"},@{p="AccountingRegisterManager";c="Manager"}, + @{p="AccountingRegisterSelection";c="Selection"},@{p="AccountingRegisterExtDimensions";c="ExtDimensions"}, + @{p="AccountingRegisterList";c="List"},@{p="AccountingRegisterRecordSet";c="RecordSet"}, + @{p="AccountingRegisterRecordKey";c="RecordKey"} + ) + "CalculationRegister" = @( + @{p="CalculationRegisterRecord";c="Record"},@{p="CalculationRegisterManager";c="Manager"}, + @{p="CalculationRegisterSelection";c="Selection"},@{p="CalculationRegisterList";c="List"}, + @{p="CalculationRegisterRecordSet";c="RecordSet"},@{p="CalculationRegisterRecordKey";c="RecordKey"} + ) + "DefinedType" = @( + @{p="DefinedType";c="DefinedType"} + ) + } + + # Metadata type -> XML tag and directory + $metaInfo = @{ + "Catalog" = @{tag="Catalog";dir="Catalogs"} + "Document" = @{tag="Document";dir="Documents"} + "Enum" = @{tag="Enum";dir="Enums"} + "ChartOfAccounts" = @{tag="ChartOfAccounts";dir="ChartsOfAccounts"} + "ChartOfCharacteristicTypes" = @{tag="ChartOfCharacteristicTypes";dir="ChartsOfCharacteristicTypes"} + "ChartOfCalculationTypes" = @{tag="ChartOfCalculationTypes";dir="ChartsOfCalculationTypes"} + "ExchangePlan" = @{tag="ExchangePlan";dir="ExchangePlans"} + "BusinessProcess" = @{tag="BusinessProcess";dir="BusinessProcesses"} + "Task" = @{tag="Task";dir="Tasks"} + "InformationRegister" = @{tag="InformationRegister";dir="InformationRegisters"} + "AccumulationRegister" = @{tag="AccumulationRegister";dir="AccumulationRegisters"} + "AccountingRegister" = @{tag="AccountingRegister";dir="AccountingRegisters"} + "CalculationRegister" = @{tag="CalculationRegister";dir="CalculationRegisters"} + "DefinedType" = @{tag="DefinedType";dir="DefinedTypes"} + } + + # StandardAttribute boilerplate + $stdAttrXml = @' + + DontCheck + false + false + Auto + + + false + + + Auto + Auto + + false + Use + false + + + + Use + + + + +'@ + + $stdAttrsByType = @{ + "Catalog" = @("PredefinedDataName","Predefined","Ref","DeletionMark","IsFolder","Owner","Parent","Description","Code") + "Document" = @("Posted","Ref","DeletionMark","Date","Number") + "Enum" = @("Order","Ref") + "ChartOfAccounts" = @("PredefinedDataName","Predefined","Ref","DeletionMark","Description","Code","Parent","Order","Type","OffBalance") + "ChartOfCharacteristicTypes" = @("PredefinedDataName","Predefined","Ref","DeletionMark","Description","Code","Parent","ValueType") + "ChartOfCalculationTypes" = @("PredefinedDataName","Predefined","Ref","DeletionMark","Description","Code","ActionPeriodIsBasic") + "ExchangePlan" = @("Ref","DeletionMark","Code","Description","ThisNode","SentNo","ReceivedNo") + "BusinessProcess" = @("Ref","DeletionMark","Date","Number","Started","Completed","HeadTask") + "Task" = @("Ref","DeletionMark","Date","Number","Executed","Description","RoutePoint","BusinessProcess") + "InformationRegister" = @("Active","LineNumber","Recorder","Period") + "AccumulationRegister" = @("Active","LineNumber","Recorder","Period") + "AccountingRegister" = @("Active","Period","Recorder","LineNumber","Account") + "CalculationRegister" = @("Active","Recorder","LineNumber","RegistrationPeriod","CalculationType","ReversingEntry") + } + + function Build-StdAttrs([string]$metaType) { + $attrs = $stdAttrsByType[$metaType] + if (-not $attrs) { return "" } + $sb = New-Object System.Text.StringBuilder + $sb.AppendLine("`t`t`t") | Out-Null + foreach ($a in $attrs) { + $sb.AppendLine("`t`t`t`t") | Out-Null + $sb.AppendLine($stdAttrXml) | Out-Null + $sb.AppendLine("`t`t`t`t") | Out-Null + } + $sb.AppendLine("`t`t`t") | Out-Null + return $sb.ToString() + } + + # --- 4a. Configuration.xml --- + $uuidCfg = [guid]::NewGuid().ToString() + $uuidLang = [guid]::NewGuid().ToString() + + $coIds = @() + for ($i = 0; $i -lt 7; $i++) { $coIds += [guid]::NewGuid().ToString() } + $classIds = @( + "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" + ) + + $coXml = "" + for ($i = 0; $i -lt 7; $i++) { + $coXml += "`r`n`t`t`t`r`n`t`t`t`t$($classIds[$i])`r`n`t`t`t`t$($coIds[$i])`r`n`t`t`t" + } + + # ChildObjects entries + $childXml = "`r`n`t`t`tРусский" + foreach ($metaType in $typeMap.Keys) { + if (-not $metaInfo.ContainsKey($metaType)) { continue } + $tag = $metaInfo[$metaType].tag + foreach ($name in $typeMap[$metaType].Keys) { + $childXml += "`r`n`t`t`t<$tag>$name" + } + } + + $cfgXml = @" + + + + $coXml + + + StubConfig + + + + Version8_3_24 + ManagedApplication + + PlatformApplication + + Russian + + + + + false + false + false + + + + + + + + + + + + + + + + + + + + + + + + Normal + + + Language.Русский + + + + + + Managed + NotAutoFree + DontUse + DontUse + Taxi + DontUse + Version8_3_24 + + + $childXml + + + +"@ + + [System.IO.File]::WriteAllText((Join-Path $cfgDir "Configuration.xml"), $cfgXml, $enc) + + # --- 4b. Language --- + $langDir = Join-Path $cfgDir "Languages" + New-Item -ItemType Directory -Path $langDir -Force | Out-Null + + $langXml = @" + + + + + Русский + + + ru + Русский + + + + ru + + + +"@ + [System.IO.File]::WriteAllText((Join-Path $langDir "Русский.xml"), $langXml, $enc) + + # --- 4c. Metadata object stubs --- + foreach ($metaType in $typeMap.Keys) { + if (-not $metaInfo.ContainsKey($metaType)) { continue } + $info = $metaInfo[$metaType] + $objDir = Join-Path $cfgDir $info.dir + New-Item -ItemType Directory -Path $objDir -Force | Out-Null + + foreach ($objName in $typeMap[$metaType].Keys) { + $uuid = [guid]::NewGuid().ToString() + + # InternalInfo with GeneratedTypes + $internalXml = "" + $gts = $gtDefs[$metaType] + if ($gts) { + $internalXml = "`r`n`t`t" + if ($metaType -eq "ExchangePlan") { + $internalXml += "`r`n`t`t`t$([guid]::NewGuid().ToString())" + } + foreach ($gt in $gts) { + $fullName = "$($gt.p).$objName" + $tid = [guid]::NewGuid().ToString() + $vid = [guid]::NewGuid().ToString() + $internalXml += "`r`n`t`t`t" + $internalXml += "`r`n`t`t`t`t$tid" + $internalXml += "`r`n`t`t`t`t$vid" + $internalXml += "`r`n`t`t`t" + } + $internalXml += "`r`n`t`t" + } + + # Properties + ChildObjects depending on type + $propsXml = "" + $childObjXml = "" + + switch ($metaType) { + "Catalog" { + $stdAttrs = Build-StdAttrs "Catalog" + $propsXml = @" + $objName + + + false + HierarchyFoldersAndItems + false + 2 + true + false + + ToItems + 9 + 25 + String + Variable + WholeCatalog + false + true + AsDescription +$stdAttrs + Auto + InDialog + true + BothWays + + Begin + DontUse + Directly + + + + + + + + + + + false + + + Automatic + Use + + + + + + DontUse + Auto + DontUse + false + false +"@ + } + "Document" { + $stdAttrs = Build-StdAttrs "Document" + $regRecordsXml = "" + # If this is the stub registrator, set register records + if ($objName -eq "ЗаглушкаРегистратора") { + $rrLines = @() + foreach ($rt in $registratorTypes) { + if ($typeMap.ContainsKey($rt) -and $typeMap[$rt].Count -gt 0) { + foreach ($rn in $typeMap[$rt].Keys) { + $rrLines += "`t`t`t`t$rt.$rn" + } + } + } + if ($rrLines.Count -gt 0) { + $regRecordsXml = "`r`n$($rrLines -join "`r`n")`r`n`t`t`t" + } + } + $propsXml = @" + $objName + + + false + + String + 11 + Variable + Year + false + true +$stdAttrs + + + DontUse + Begin + DontUse + Directly + + + + + + + Allow + Deny + AutoDelete + WriteModified + AutoFill + $regRecordsXml + true + true + false + + Automatic + Use + + + + + + Auto + DontUse + false + false +"@ + } + "Enum" { + $stdAttrs = Build-StdAttrs "Enum" + $propsXml = @" + $objName + + + false +$stdAttrs + true + BothWays + + + + + + + + Auto +"@ + } + "InformationRegister" { + $stdAttrs = Build-StdAttrs "InformationRegister" + $propsXml = @" + $objName + + + false + InDialog + + + + +$stdAttrs Nonperiodical + Independent + false + false + Automatic + Use + false + false + + + + + + DontUse + false + false +"@ + } + "AccumulationRegister" { + $stdAttrs = Build-StdAttrs "AccumulationRegister" + $propsXml = @" + $objName + + + false + + + Balance + false +$stdAttrs Automatic + Use + true + + + +"@ + } + "AccountingRegister" { + $stdAttrs = Build-StdAttrs "AccountingRegister" + $propsXml = @" + $objName + + + false + + + false + + false +$stdAttrs Automatic + Use + true + + + +"@ + } + "CalculationRegister" { + $stdAttrs = Build-StdAttrs "CalculationRegister" + $propsXml = @" + $objName + + + false + + + false + +$stdAttrs Automatic + Use + + + +"@ + } + "ChartOfAccounts" { + $stdAttrs = Build-StdAttrs "ChartOfAccounts" + $propsXml = @" + $objName + + + false + + 20 + 100 + WholeCatalog + false + true + AsDescription +$stdAttrs + Auto + InDialog + true + BothWays + + Begin + DontUse + Directly + + + + + + + + + + + true + 5 + 0 + false + + + Automatic + Use + + + + + + DontUse + Auto + DontUse + false + false +"@ + } + "ChartOfCharacteristicTypes" { + $stdAttrs = Build-StdAttrs "ChartOfCharacteristicTypes" + $propsXml = @" + $objName + + + false + 9 + Variable + 25 + false + true + AsDescription + + + xs:boolean + xs:string + + 0 + Variable + + xs:decimal + + 15 + 2 + Any + + xs:dateTime + + DateTime + + + false + true +$stdAttrs + Auto + InDialog + true + BothWays + + Begin + DontUse + Directly + + + + + + + + + + + false + + + Automatic + Use + + + + + + DontUse + Auto + DontUse + false + false +"@ + } + "ChartOfCalculationTypes" { + $stdAttrs = Build-StdAttrs "ChartOfCalculationTypes" + $propsXml = @" + $objName + + + false + 9 + 25 + String + Variable + WholeCatalog + false + true + AsDescription +$stdAttrs + Auto + InDialog + true + BothWays + + Begin + DontUse + Directly + NotDepend + + false + + + + + + + false + + + Automatic + Use + + + + + + DontUse + Auto + DontUse + false + false +"@ + } + "ExchangePlan" { + $stdAttrs = Build-StdAttrs "ExchangePlan" + $propsXml = @" + $objName + + + false + 9 + 25 + Variable +$stdAttrs AsDescription + + Auto + InDialog + true + BothWays + + Begin + DontUse + Directly + + + + + + + false + + + Automatic + Use + + + + + + DontUse + Auto + false + DontUse + false + false +"@ + } + "BusinessProcess" { + $stdAttrs = Build-StdAttrs "BusinessProcess" + $propsXml = @" + $objName + + + false + + String + 11 + Variable + Year + false + true +$stdAttrs + + false + + + + + + + false + + + Automatic + Use + + + + + + Auto + DontUse + false + false +"@ + } + "Task" { + $stdAttrs = Build-StdAttrs "Task" + $propsXml = @" + $objName + + + false + + String + 11 + Variable + Year + false + true + 25 +$stdAttrs + + Begin + DontUse + Directly + + + + + + + false + + + Automatic + Use + + + + + + + + + + Auto + DontUse + false + false +"@ + } + "DefinedType" { + $propsXml = @" + $objName + + + + xs:string + + 0 + Variable + + +"@ + } + } + + $childObjLine = "`n`t`t" + if ($metaType -eq "DefinedType") { + $childObjLine = "" + } elseif ($metaType -eq "InformationRegister") { + # Check if we have actual column names from form scanning + $regKey = "InformationRegister.$objName" + $cols = if ($registerColumns.ContainsKey($regKey) -and $registerColumns[$regKey].Count -gt 0) { + $registerColumns[$regKey].Keys + } else { + @("Заглушка") + } + # First column as Dimension (for MainFilter), rest as Attributes (no index pressure) + $dimXmlParts = @() + $isFirst = $true + foreach ($colName in $cols) { + $elemUuid = [guid]::NewGuid().ToString() + if ($isFirst) { + $dimXmlParts += @" + + + $colName + + + + xs:string + + 10 + Variable + + + false + + + + false + + false + false + + + false + + DontCheck + Items + + + Auto + Auto + + + Auto + false + true + false + DontIndex + Use + Use + + +"@ + $isFirst = $false + } else { + $dimXmlParts += @" + + + $colName + + + + xs:string + + 10 + Variable + + + false + + + + false + + false + false + + + false + + DontCheck + Items + + + Auto + Auto + + + Auto + Use + + +"@ + } + } + $childObjLine = "`r`n`t`t`r`n$($dimXmlParts -join "`r`n")`r`n`t`t" + } elseif ($metaType -in @("AccumulationRegister","AccountingRegister","CalculationRegister")) { + # Check if we have actual column names from form scanning + $regKey = "$metaType.$objName" + $cols = if ($registerColumns.ContainsKey($regKey) -and $registerColumns[$regKey].Count -gt 0) { + $registerColumns[$regKey].Keys + } else { + @() + } + $childParts = @() + # AccumulationRegister requires at least one Resource + $stubResUuid = [guid]::NewGuid().ToString() + $childParts += @" + + + Заглушка + + + + xs:decimal + + 15 + 2 + Any + + + false + + + + false + + false + false + + + DontCheck + Items + + + Auto + Auto + + + Auto + Use + + +"@ + # Add all form-referenced columns as Dimensions (short strings to avoid index overflow) + foreach ($colName in $cols) { + $dimUuid = [guid]::NewGuid().ToString() + $childParts += @" + + + $colName + + + + xs:string + + 10 + Variable + + + false + + + + false + + false + false + + + DontCheck + Items + + + Auto + Auto + + + Auto + Use + + +"@ + } + $childObjLine = "`r`n`t`t`r`n$($childParts -join "`r`n")`r`n`t`t" + } + $objXml = @" + + + <$($info.tag) uuid="$uuid">$internalXml + +$propsXml $childObjLine + + +"@ + [System.IO.File]::WriteAllText((Join-Path $objDir "$objName.xml"), $objXml, $enc) + } + } + + Write-Host "Generated stub configuration with $($typeMap.Count) metadata types" + if ($registerColumns.Count -gt 0) { + Write-Host "WARNING: Register column categories (Dimension/Resource/Attribute) are guessed. Form field bindings may not survive round-trip through a real database." -ForegroundColor Yellow + } +} + +# --- 5. Create infobase --- +Write-Host "Creating infobase: $TempBasePath" +$createArgs = "CREATEINFOBASE File=`"$TempBasePath`" /DisableStartupDialogs" +$proc = Start-Process -FilePath $V8Path -ArgumentList $createArgs -NoNewWindow -Wait -PassThru +if ($proc.ExitCode -ne 0) { + Write-Error "Failed to create infobase (code: $($proc.ExitCode))" + exit 1 +} + +# --- 6. Load config and update DB if ref types exist --- +if ($hasRefTypes) { + $cfgDir = Join-Path $TempBasePath "cfg" + # LoadConfigFromFiles + Write-Host "Loading configuration from files..." + $loadLog = Join-Path $env:TEMP "stub_load_log.txt" + $loadArgs = "DESIGNER /F`"$TempBasePath`" /LoadConfigFromFiles `"$cfgDir`" /Out `"$loadLog`" /DisableStartupDialogs" + $proc = Start-Process -FilePath $V8Path -ArgumentList $loadArgs -NoNewWindow -Wait -PassThru + if ($proc.ExitCode -ne 0) { + if (Test-Path $loadLog) { Get-Content $loadLog -Raw -ErrorAction SilentlyContinue | Write-Host } + Write-Error "Failed to load config (code: $($proc.ExitCode))" + exit 1 + } + + # UpdateDBCfg + Write-Host "Updating database configuration..." + $updateLog = Join-Path $env:TEMP "stub_update_log.txt" + $updateArgs = "DESIGNER /F`"$TempBasePath`" /UpdateDBCfg /Out `"$updateLog`" /DisableStartupDialogs" + $proc = Start-Process -FilePath $V8Path -ArgumentList $updateArgs -NoNewWindow -Wait -PassThru + if ($proc.ExitCode -ne 0) { + if (Test-Path $updateLog) { Get-Content $updateLog -Raw -ErrorAction SilentlyContinue | Write-Host } + Write-Error "Failed to update DB config (code: $($proc.ExitCode))" + exit 1 + } + + # Cleanup cfg dir + Remove-Item -Path $cfgDir -Recurse -Force -ErrorAction SilentlyContinue +} + +# --- 7. Output base path --- +Write-Host "[OK] Stub database created: $TempBasePath" +Write-Host $TempBasePath diff --git a/.codex/skills/epf-build/scripts/stub-db-create.py b/.codex/skills/epf-build/scripts/stub-db-create.py new file mode 100644 index 00000000..2f9882e0 --- /dev/null +++ b/.codex/skills/epf-build/scripts/stub-db-create.py @@ -0,0 +1,1085 @@ +#!/usr/bin/env python3 +# stub-db-create v1.0 — Create temp 1C infobase with metadata stubs for EPF/ERF build +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills + +import argparse +import os +import random +import re +import subprocess +import sys +import tempfile +import uuid + + +def new_uuid(): + return str(uuid.uuid4()) + + +def scan_ref_types(source_dir): + """Scan XML files for reference/object/recordset types. Returns {metaType: {name: True}}.""" + type_map = {} + + ref_pattern = re.compile( + r'(?:cfg:|d\dp1:)(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef' + r'|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef' + r'|ExchangePlanRef|BusinessProcessRef|TaskRef)' + r'\.([A-Za-z\u0400-\u04FF\d_]+)' + ) + obj_pattern = re.compile( + r'(?:cfg:|d\dp1:)(CatalogObject|DocumentObject|ChartOfAccountsObject' + r'|ChartOfCharacteristicTypesObject|ChartOfCalculationTypesObject' + r'|ExchangePlanObject|BusinessProcessObject|TaskObject)' + r'\.([A-Za-z\u0400-\u04FF\d_]+)' + ) + rs_pattern = re.compile( + r'(?:cfg:|d\dp1:)(InformationRegisterRecordSet|AccumulationRegisterRecordSet' + r'|AccountingRegisterRecordSet|CalculationRegisterRecordSet)' + r'\.([A-Za-z\u0400-\u04FF\d_]+)' + ) + char_pattern = re.compile(r'cfg:Characteristic\.([A-Za-z\u0400-\u04FF\d_]+)') + dt_pattern = re.compile(r'cfg:DefinedType\.([A-Za-z\u0400-\u04FF\d_]+)') + + ref_map = { + 'CatalogRef': 'Catalog', 'DocumentRef': 'Document', 'EnumRef': 'Enum', + 'ChartOfAccountsRef': 'ChartOfAccounts', + 'ChartOfCharacteristicTypesRef': 'ChartOfCharacteristicTypes', + 'ChartOfCalculationTypesRef': 'ChartOfCalculationTypes', + 'ExchangePlanRef': 'ExchangePlan', 'BusinessProcessRef': 'BusinessProcess', 'TaskRef': 'Task', + } + obj_map = { + 'CatalogObject': 'Catalog', 'DocumentObject': 'Document', + 'ChartOfAccountsObject': 'ChartOfAccounts', + 'ChartOfCharacteristicTypesObject': 'ChartOfCharacteristicTypes', + 'ChartOfCalculationTypesObject': 'ChartOfCalculationTypes', + 'ExchangePlanObject': 'ExchangePlan', 'BusinessProcessObject': 'BusinessProcess', 'TaskObject': 'Task', + } + rs_map = { + 'InformationRegisterRecordSet': 'InformationRegister', + 'AccumulationRegisterRecordSet': 'AccumulationRegister', + 'AccountingRegisterRecordSet': 'AccountingRegister', + 'CalculationRegisterRecordSet': 'CalculationRegister', + } + + for dirpath, _, filenames in os.walk(source_dir): + for fn in filenames: + if not fn.endswith('.xml'): + continue + fp = os.path.join(dirpath, fn) + try: + with open(fp, 'r', encoding='utf-8-sig') as f: + content = f.read() + except Exception: + continue + + for m in ref_pattern.finditer(content): + mt = ref_map[m.group(1)] + type_map.setdefault(mt, {})[m.group(2)] = True + for m in obj_pattern.finditer(content): + mt = obj_map[m.group(1)] + type_map.setdefault(mt, {})[m.group(2)] = True + for m in rs_pattern.finditer(content): + mt = rs_map[m.group(1)] + type_map.setdefault(mt, {})[m.group(2)] = True + for m in char_pattern.finditer(content): + type_map.setdefault('ChartOfCharacteristicTypes', {})[m.group(1)] = True + for m in dt_pattern.finditer(content): + type_map.setdefault('DefinedType', {})[m.group(1)] = True + + return type_map + + +def scan_register_columns(source_dir): + """Scan Form.xml for register record set columns referenced via DataPath. + Returns {"RegisterType.RegisterName": {"col1": True, "col2": True}}.""" + import xml.etree.ElementTree as ET + + register_columns = {} + std_cols = {'LineNumber', 'Period', 'Recorder', 'Active', 'RecordType'} + rs_type_map = { + 'InformationRegisterRecordSet': 'InformationRegister', + 'AccumulationRegisterRecordSet': 'AccumulationRegister', + 'AccountingRegisterRecordSet': 'AccountingRegister', + 'CalculationRegisterRecordSet': 'CalculationRegister', + } + rs_pattern = re.compile( + r'^(?:cfg:|d\dp1:)(InformationRegisterRecordSet|AccumulationRegisterRecordSet' + r'|AccountingRegisterRecordSet|CalculationRegisterRecordSet)\.(.+)$' + ) + dp_pattern = re.compile(r'([A-Za-z\u0400-\u04FF\d_]+)\.([A-Za-z\u0400-\u04FF\d_]+)') + + ns = { + 'v8': 'http://v8.1c.ru/8.1/data/core', + 'f': 'http://v8.1c.ru/8.3/xcf/logform', + } + + for dirpath, _, filenames in os.walk(source_dir): + for fn in filenames: + if fn != 'Form.xml': + continue + fp = os.path.join(dirpath, fn) + try: + with open(fp, 'r', encoding='utf-8-sig') as fh: + content = fh.read() + except Exception: + continue + if '' not in content: + continue + + # Parse form attributes to find register recordset types + reg_attr_map = {} # formAttrName -> "RegisterType.RegisterName" + try: + root = ET.fromstring(content) + for attr_node in root.iter('{http://v8.1c.ru/8.3/xcf/logform}Attribute'): + attr_name = attr_node.get('name', '') + for type_node in attr_node.iter('{http://v8.1c.ru/8.1/data/core}Type'): + m = rs_pattern.match(type_node.text or '') + if m: + reg_type = rs_type_map[m.group(1)] + reg_key = f"{reg_type}.{m.group(2)}" + reg_attr_map[attr_name] = reg_key + register_columns.setdefault(reg_key, {}) + except Exception: + continue + + # Find DataPath references like "AttrName.ColumnName" + for m in dp_pattern.finditer(content): + attr_name, col_name = m.group(1), m.group(2) + if attr_name in reg_attr_map and col_name not in std_cols: + register_columns[reg_attr_map[attr_name]][col_name] = True + + return register_columns + + +NS = ( + '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" version="2.17"' +) + +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", +] + +GT_DEFS = { + 'Catalog': [('CatalogObject','Object'),('CatalogRef','Ref'),('CatalogSelection','Selection'),('CatalogList','List'),('CatalogManager','Manager')], + 'Document': [('DocumentObject','Object'),('DocumentRef','Ref'),('DocumentSelection','Selection'),('DocumentList','List'),('DocumentManager','Manager')], + 'Enum': [('EnumRef','Ref'),('EnumManager','Manager'),('EnumList','List')], + 'ChartOfAccounts': [('ChartOfAccountsObject','Object'),('ChartOfAccountsRef','Ref'),('ChartOfAccountsSelection','Selection'),('ChartOfAccountsList','List'),('ChartOfAccountsManager','Manager')], + 'ChartOfCharacteristicTypes': [('ChartOfCharacteristicTypesObject','Object'),('ChartOfCharacteristicTypesRef','Ref'),('ChartOfCharacteristicTypesSelection','Selection'),('ChartOfCharacteristicTypesList','List'),('Characteristic','Characteristic'),('ChartOfCharacteristicTypesManager','Manager')], + 'ChartOfCalculationTypes': [('ChartOfCalculationTypesObject','Object'),('ChartOfCalculationTypesRef','Ref'),('ChartOfCalculationTypesSelection','Selection'),('ChartOfCalculationTypesList','List'),('ChartOfCalculationTypesManager','Manager')], + 'ExchangePlan': [('ExchangePlanObject','Object'),('ExchangePlanRef','Ref'),('ExchangePlanSelection','Selection'),('ExchangePlanList','List'),('ExchangePlanManager','Manager')], + 'BusinessProcess': [('BusinessProcessObject','Object'),('BusinessProcessRef','Ref'),('BusinessProcessSelection','Selection'),('BusinessProcessList','List'),('BusinessProcessManager','Manager')], + 'Task': [('TaskObject','Object'),('TaskRef','Ref'),('TaskSelection','Selection'),('TaskList','List'),('TaskManager','Manager')], + 'InformationRegister': [('InformationRegisterRecord','Record'),('InformationRegisterManager','Manager'),('InformationRegisterSelection','Selection'),('InformationRegisterList','List'),('InformationRegisterRecordSet','RecordSet'),('InformationRegisterRecordKey','RecordKey'),('InformationRegisterRecordManager','RecordManager')], + 'AccumulationRegister': [('AccumulationRegisterRecord','Record'),('AccumulationRegisterManager','Manager'),('AccumulationRegisterSelection','Selection'),('AccumulationRegisterList','List'),('AccumulationRegisterRecordSet','RecordSet'),('AccumulationRegisterRecordKey','RecordKey')], + 'AccountingRegister': [('AccountingRegisterRecord','Record'),('AccountingRegisterManager','Manager'),('AccountingRegisterSelection','Selection'),('AccountingRegisterExtDimensions','ExtDimensions'),('AccountingRegisterList','List'),('AccountingRegisterRecordSet','RecordSet'),('AccountingRegisterRecordKey','RecordKey')], + 'CalculationRegister': [('CalculationRegisterRecord','Record'),('CalculationRegisterManager','Manager'),('CalculationRegisterSelection','Selection'),('CalculationRegisterList','List'),('CalculationRegisterRecordSet','RecordSet'),('CalculationRegisterRecordKey','RecordKey')], + 'DefinedType': [('DefinedType','DefinedType')], +} + +META_INFO = { + 'Catalog': ('Catalog', 'Catalogs'), + 'Document': ('Document', 'Documents'), + 'Enum': ('Enum', 'Enums'), + 'ChartOfAccounts': ('ChartOfAccounts', 'ChartsOfAccounts'), + 'ChartOfCharacteristicTypes': ('ChartOfCharacteristicTypes', 'ChartsOfCharacteristicTypes'), + 'ChartOfCalculationTypes': ('ChartOfCalculationTypes', 'ChartsOfCalculationTypes'), + 'ExchangePlan': ('ExchangePlan', 'ExchangePlans'), + 'BusinessProcess': ('BusinessProcess', 'BusinessProcesses'), + 'Task': ('Task', 'Tasks'), + 'InformationRegister': ('InformationRegister', 'InformationRegisters'), + 'AccumulationRegister': ('AccumulationRegister', 'AccumulationRegisters'), + 'AccountingRegister': ('AccountingRegister', 'AccountingRegisters'), + 'CalculationRegister': ('CalculationRegister', 'CalculationRegisters'), + 'DefinedType': ('DefinedType', 'DefinedTypes'), +} + +STD_ATTRS_BY_TYPE = { + 'Catalog': ['PredefinedDataName','Predefined','Ref','DeletionMark','IsFolder','Owner','Parent','Description','Code'], + 'Document': ['Posted','Ref','DeletionMark','Date','Number'], + 'Enum': ['Order','Ref'], + 'ChartOfAccounts': ['PredefinedDataName','Predefined','Ref','DeletionMark','Description','Code','Parent','Order','Type','OffBalance'], + 'ChartOfCharacteristicTypes': ['PredefinedDataName','Predefined','Ref','DeletionMark','Description','Code','Parent','ValueType'], + 'ChartOfCalculationTypes': ['PredefinedDataName','Predefined','Ref','DeletionMark','Description','Code','ActionPeriodIsBasic'], + 'ExchangePlan': ['Ref','DeletionMark','Code','Description','ThisNode','SentNo','ReceivedNo'], + 'BusinessProcess': ['Ref','DeletionMark','Date','Number','Started','Completed','HeadTask'], + 'Task': ['Ref','DeletionMark','Date','Number','Executed','Description','RoutePoint','BusinessProcess'], + 'InformationRegister': ['Active','LineNumber','Recorder','Period'], + 'AccumulationRegister': ['Active','LineNumber','Recorder','Period'], + 'AccountingRegister': ['Active','Period','Recorder','LineNumber','Account'], + 'CalculationRegister': ['Active','Recorder','LineNumber','RegistrationPeriod','CalculationType','ReversingEntry'], +} + +STD_ATTR_BODY = """\t\t\t\t +\t\t\t\tDontCheck +\t\t\t\tfalse +\t\t\t\tfalse +\t\t\t\tAuto +\t\t\t\t +\t\t\t\t +\t\t\t\tfalse +\t\t\t\t +\t\t\t\t +\t\t\t\tAuto +\t\t\t\tAuto +\t\t\t\t +\t\t\t\tfalse +\t\t\t\tUse +\t\t\t\tfalse +\t\t\t\t +\t\t\t\t +\t\t\t\t +\t\t\t\tUse +\t\t\t\t +\t\t\t\t +\t\t\t\t +\t\t\t\t""" + + +def build_std_attrs(meta_type): + attrs = STD_ATTRS_BY_TYPE.get(meta_type) + if not attrs: + return '' + lines = ['\t\t\t'] + for a in attrs: + lines.append(f'\t\t\t\t') + lines.append(STD_ATTR_BODY) + lines.append(f'\t\t\t\t') + lines.append('\t\t\t') + return '\n'.join(lines) + '\n' + + +def build_internal_info(meta_type, obj_name): + gts = GT_DEFS.get(meta_type) + if not gts: + return '' + lines = ['\t\t'] + if meta_type == 'ExchangePlan': + lines.append(f'\t\t\t{new_uuid()}') + for prefix, cat in gts: + full = f'{prefix}.{obj_name}' + lines.append(f'\t\t\t') + lines.append(f'\t\t\t\t{new_uuid()}') + lines.append(f'\t\t\t\t{new_uuid()}') + lines.append(f'\t\t\t') + lines.append('\t\t') + return '\n'.join(lines) + + +# Properties templates per type — returns the Properties content (without tags) +PROPS = {} + +PROPS['Catalog'] = lambda n, sa: f"""\t\t\t{n} +\t\t\t +\t\t\t +\t\t\tfalse +\t\t\tHierarchyFoldersAndItems +\t\t\tfalse +\t\t\t2 +\t\t\ttrue +\t\t\tfalse +\t\t\t +\t\t\tToItems +\t\t\t9 +\t\t\t25 +\t\t\tString +\t\t\tVariable +\t\t\tWholeCatalog +\t\t\tfalse +\t\t\ttrue +\t\t\tAsDescription +{sa}\t\t\t +\t\t\tAuto +\t\t\tInDialog +\t\t\ttrue +\t\t\tBothWays +\t\t\t +\t\t\tBegin +\t\t\tDontUse +\t\t\tDirectly +\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\tfalse +\t\t\t +\t\t\t +\t\t\tAutomatic +\t\t\tUse +\t\t\t +\t\t\t +\t\t\t +\t\t\t +\t\t\t +\t\t\tDontUse +\t\t\tAuto +\t\t\tDontUse +\t\t\tfalse +\t\t\tfalse""" + +PROPS['Enum'] = lambda n, sa: f"""\t\t\t{n} +\t\t\t +\t\t\t +\t\t\tfalse +{sa}\t\t\t +\t\t\ttrue +\t\t\tBothWays +\t\t\t +\t\t\t +\t\t\t +\t\t\t +\t\t\t +\t\t\t +\t\t\t +\t\t\tAuto""" + +PROPS['InformationRegister'] = lambda n, sa: f"""\t\t\t{n} +\t\t\t +\t\t\t +\t\t\tfalse +\t\t\tInDialog +\t\t\t +\t\t\t +\t\t\t +\t\t\t +{sa}\t\t\tNonperiodical +\t\t\tIndependent +\t\t\tfalse +\t\t\tfalse +\t\t\tAutomatic +\t\t\tUse +\t\t\tfalse +\t\t\tfalse +\t\t\t +\t\t\t +\t\t\t +\t\t\t +\t\t\t +\t\t\tDontUse +\t\t\tfalse +\t\t\tfalse""" + +PROPS['AccumulationRegister'] = lambda n, sa: f"""\t\t\t{n} +\t\t\t +\t\t\t +\t\t\tfalse +\t\t\t +\t\t\t +\t\t\tBalance +\t\t\tfalse +{sa}\t\t\tAutomatic +\t\t\tUse +\t\t\ttrue +\t\t\t +\t\t\t +\t\t\t""" + +PROPS['AccountingRegister'] = lambda n, sa: f"""\t\t\t{n} +\t\t\t +\t\t\t +\t\t\tfalse +\t\t\t +\t\t\t +\t\t\tfalse +\t\t\t +\t\t\tfalse +{sa}\t\t\tAutomatic +\t\t\tUse +\t\t\ttrue +\t\t\t +\t\t\t +\t\t\t""" + +PROPS['CalculationRegister'] = lambda n, sa: f"""\t\t\t{n} +\t\t\t +\t\t\t +\t\t\tfalse +\t\t\t +\t\t\t +\t\t\tfalse +\t\t\t +{sa}\t\t\tAutomatic +\t\t\tUse +\t\t\t +\t\t\t +\t\t\t""" + +PROPS['ChartOfAccounts'] = lambda n, sa: f"""\t\t\t{n} +\t\t\t +\t\t\t +\t\t\tfalse +\t\t\t +\t\t\t20 +\t\t\t100 +\t\t\tWholeCatalog +\t\t\tfalse +\t\t\ttrue +\t\t\tAsDescription +{sa}\t\t\t +\t\t\tAuto +\t\t\tInDialog +\t\t\ttrue +\t\t\tBothWays +\t\t\t +\t\t\tBegin +\t\t\tDontUse +\t\t\tDirectly +\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\ttrue +\t\t\t5 +\t\t\t0 +\t\t\tfalse +\t\t\t +\t\t\t +\t\t\tAutomatic +\t\t\tUse +\t\t\t +\t\t\t +\t\t\t +\t\t\t +\t\t\t +\t\t\tDontUse +\t\t\tAuto +\t\t\tDontUse +\t\t\tfalse +\t\t\tfalse""" + +PROPS['ChartOfCharacteristicTypes'] = lambda n, sa: f"""\t\t\t{n} +\t\t\t +\t\t\t +\t\t\tfalse +\t\t\t9 +\t\t\tVariable +\t\t\t25 +\t\t\tfalse +\t\t\ttrue +\t\t\tAsDescription +\t\t\t +\t\t\t +\t\t\t\txs:boolean +\t\t\t\txs:string +\t\t\t\t +\t\t\t\t\t0 +\t\t\t\t\tVariable +\t\t\t\t +\t\t\t\txs:decimal +\t\t\t\t +\t\t\t\t\t15 +\t\t\t\t\t2 +\t\t\t\t\tAny +\t\t\t\t +\t\t\t\txs:dateTime +\t\t\t\t +\t\t\t\t\tDateTime +\t\t\t\t +\t\t\t +\t\t\tfalse +\t\t\ttrue +{sa}\t\t\t +\t\t\tAuto +\t\t\tInDialog +\t\t\ttrue +\t\t\tBothWays +\t\t\t +\t\t\tBegin +\t\t\tDontUse +\t\t\tDirectly +\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\tfalse +\t\t\t +\t\t\t +\t\t\tAutomatic +\t\t\tUse +\t\t\t +\t\t\t +\t\t\t +\t\t\t +\t\t\t +\t\t\tDontUse +\t\t\tAuto +\t\t\tDontUse +\t\t\tfalse +\t\t\tfalse""" + +PROPS['ChartOfCalculationTypes'] = lambda n, sa: f"""\t\t\t{n} +\t\t\t +\t\t\t +\t\t\tfalse +\t\t\t9 +\t\t\t25 +\t\t\tString +\t\t\tVariable +\t\t\tWholeCatalog +\t\t\tfalse +\t\t\ttrue +\t\t\tAsDescription +{sa}\t\t\t +\t\t\tAuto +\t\t\tInDialog +\t\t\ttrue +\t\t\tBothWays +\t\t\t +\t\t\tBegin +\t\t\tDontUse +\t\t\tDirectly +\t\t\tNotDepend +\t\t\t +\t\t\tfalse +\t\t\t +\t\t\t +\t\t\t +\t\t\t +\t\t\t +\t\t\t +\t\t\tfalse +\t\t\t +\t\t\t +\t\t\tAutomatic +\t\t\tUse +\t\t\t +\t\t\t +\t\t\t +\t\t\t +\t\t\t +\t\t\tDontUse +\t\t\tAuto +\t\t\tDontUse +\t\t\tfalse +\t\t\tfalse""" + +PROPS['ExchangePlan'] = lambda n, sa: f"""\t\t\t{n} +\t\t\t +\t\t\t +\t\t\tfalse +\t\t\t9 +\t\t\t25 +\t\t\tVariable +{sa}\t\t\tAsDescription +\t\t\t +\t\t\tAuto +\t\t\tInDialog +\t\t\ttrue +\t\t\tBothWays +\t\t\t +\t\t\tBegin +\t\t\tDontUse +\t\t\tDirectly +\t\t\t +\t\t\t +\t\t\t +\t\t\t +\t\t\t +\t\t\t +\t\t\tfalse +\t\t\t +\t\t\t +\t\t\tAutomatic +\t\t\tUse +\t\t\t +\t\t\t +\t\t\t +\t\t\t +\t\t\t +\t\t\tDontUse +\t\t\tAuto +\t\t\tfalse +\t\t\tDontUse +\t\t\tfalse +\t\t\tfalse""" + +PROPS['BusinessProcess'] = lambda n, sa: f"""\t\t\t{n} +\t\t\t +\t\t\t +\t\t\tfalse +\t\t\t +\t\t\tString +\t\t\t11 +\t\t\tVariable +\t\t\tYear +\t\t\tfalse +\t\t\ttrue +{sa}\t\t\t +\t\t\t +\t\t\tfalse +\t\t\t +\t\t\t +\t\t\t +\t\t\t +\t\t\t +\t\t\t +\t\t\tfalse +\t\t\t +\t\t\t +\t\t\tAutomatic +\t\t\tUse +\t\t\t +\t\t\t +\t\t\t +\t\t\t +\t\t\t +\t\t\tAuto +\t\t\tDontUse +\t\t\tfalse +\t\t\tfalse""" + +PROPS['Task'] = lambda n, sa: f"""\t\t\t{n} +\t\t\t +\t\t\t +\t\t\tfalse +\t\t\t +\t\t\tString +\t\t\t11 +\t\t\tVariable +\t\t\tYear +\t\t\tfalse +\t\t\ttrue +\t\t\t25 +{sa}\t\t\t +\t\t\t +\t\t\tBegin +\t\t\tDontUse +\t\t\tDirectly +\t\t\t +\t\t\t +\t\t\t +\t\t\t +\t\t\t +\t\t\t +\t\t\tfalse +\t\t\t +\t\t\t +\t\t\tAutomatic +\t\t\tUse +\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\tAuto +\t\t\tDontUse +\t\t\tfalse +\t\t\tfalse""" + +PROPS['DefinedType'] = lambda n, sa: f"""\t\t\t{n} +\t\t\t +\t\t\t +\t\t\t +\t\t\t\txs:string +\t\t\t\t +\t\t\t\t\t0 +\t\t\t\t\tVariable +\t\t\t\t +\t\t\t""" + + +def build_doc_props(obj_name, std_attrs, reg_records_xml): + return f"""\t\t\t{obj_name} +\t\t\t +\t\t\t +\t\t\tfalse +\t\t\t +\t\t\tString +\t\t\t11 +\t\t\tVariable +\t\t\tYear +\t\t\tfalse +\t\t\ttrue +{std_attrs}\t\t\t +\t\t\t +\t\t\t +\t\t\tDontUse +\t\t\tBegin +\t\t\tDontUse +\t\t\tDirectly +\t\t\t +\t\t\t +\t\t\t +\t\t\t +\t\t\t +\t\t\t +\t\t\tAllow +\t\t\tDeny +\t\t\tAutoDelete +\t\t\tWriteModified +\t\t\tAutoFill +\t\t\t{reg_records_xml} +\t\t\ttrue +\t\t\ttrue +\t\t\tfalse +\t\t\t +\t\t\tAutomatic +\t\t\tUse +\t\t\t +\t\t\t +\t\t\t +\t\t\t +\t\t\t +\t\t\tAuto +\t\t\tDontUse +\t\t\tfalse +\t\t\tfalse""" + + +def write_bom(path, content): + with open(path, 'w', encoding='utf-8-sig', newline='') as f: + f.write(content) + + +def main(): + sys.stdout.reconfigure(encoding='utf-8') + sys.stderr.reconfigure(encoding='utf-8') + + parser = argparse.ArgumentParser(description='Create temp 1C infobase with metadata stubs') + parser.add_argument('-SourceDir', required=True) + parser.add_argument('-V8Path', required=True) + parser.add_argument('-TempBasePath', default='') + args = parser.parse_args() + + type_map = scan_ref_types(args.SourceDir) + register_columns = scan_register_columns(args.SourceDir) + has_ref_types = len(type_map) > 0 + + temp_base = args.TempBasePath or os.path.join(tempfile.gettempdir(), f'epf_stub_db_{random.randint(0,999999)}') + + # Add registrator stub document if needed + registrator_types = ['AccumulationRegister', 'AccountingRegister', 'CalculationRegister'] + needs_registrator = any(rt in type_map and len(type_map[rt]) > 0 for rt in registrator_types) + if needs_registrator: + type_map.setdefault('Document', {})['\u0417\u0430\u0433\u043b\u0443\u0448\u043a\u0430\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440\u0430'] = True # ЗаглушкаРегистратора + + if has_ref_types: + cfg_dir = os.path.join(temp_base, 'cfg') + os.makedirs(cfg_dir, exist_ok=True) + + # Configuration.xml + uuid_cfg = new_uuid() + uuid_lang = new_uuid() + co_ids = [new_uuid() for _ in range(7)] + + co_xml = '' + for i in range(7): + co_xml += f'\n\t\t\t\n\t\t\t\t{CLASS_IDS[i]}\n\t\t\t\t{co_ids[i]}\n\t\t\t' + + child_xml = '\n\t\t\t\u0420\u0443\u0441\u0441\u043a\u0438\u0439' # Русский + for meta_type, names in type_map.items(): + if meta_type not in META_INFO: + continue + tag = META_INFO[meta_type][0] + for name in names: + child_xml += f'\n\t\t\t<{tag}>{name}' + + cfg_xml = f""" + +\t +\t\t{co_xml} +\t\t +\t\t +\t\t\tStubConfig +\t\t\t +\t\t\t +\t\t\t +\t\t\tVersion8_3_24 +\t\t\tManagedApplication +\t\t\t +\t\t\t\tPlatformApplication +\t\t\t +\t\t\tRussian +\t\t\t +\t\t\t +\t\t\t +\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 +\t\t\t +\t\t\t +\t\t\t +\t\t\tNormal +\t\t\t +\t\t\t +\t\t\tLanguage.\u0420\u0443\u0441\u0441\u043a\u0438\u0439 +\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\tVersion8_3_24 +\t\t\t +\t\t +\t\t{child_xml} +\t\t +\t + +""" + write_bom(os.path.join(cfg_dir, 'Configuration.xml'), cfg_xml) + + # Language + lang_dir = os.path.join(cfg_dir, 'Languages') + os.makedirs(lang_dir, exist_ok=True) + lang_xml = f""" + +\t +\t\t +\t\t\t\u0420\u0443\u0441\u0441\u043a\u0438\u0439 +\t\t\t +\t\t\t\t +\t\t\t\t\tru +\t\t\t\t\t\u0420\u0443\u0441\u0441\u043a\u0438\u0439 +\t\t\t\t +\t\t\t +\t\t\t +\t\t\tru +\t\t +\t + +""" + write_bom(os.path.join(lang_dir, '\u0420\u0443\u0441\u0441\u043a\u0438\u0439.xml'), lang_xml) + + # Metadata stubs + for meta_type, names in type_map.items(): + if meta_type not in META_INFO: + continue + tag, dirname = META_INFO[meta_type] + obj_dir = os.path.join(cfg_dir, dirname) + os.makedirs(obj_dir, exist_ok=True) + + for obj_name in names: + obj_uuid = new_uuid() + internal_xml = build_internal_info(meta_type, obj_name) + if internal_xml: + internal_xml = '\n' + internal_xml + + sa = build_std_attrs(meta_type) + + if meta_type == 'Document': + rr_xml = '' + if obj_name == '\u0417\u0430\u0433\u043b\u0443\u0448\u043a\u0430\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440\u0430': # ЗаглушкаРегистратора + rr_lines = [] + for rt in registrator_types: + if rt in type_map: + for rn in type_map[rt]: + rr_lines.append(f'\t\t\t\t{rt}.{rn}') + if rr_lines: + rr_xml = '\n' + '\n'.join(rr_lines) + '\n\t\t\t' + props_xml = build_doc_props(obj_name, sa, rr_xml) + elif meta_type in PROPS: + props_xml = PROPS[meta_type](obj_name, sa) + else: + props_xml = f'\t\t\t{obj_name}\n\t\t\t\n\t\t\t' + + # ChildObjects — varies by type + if meta_type == 'DefinedType': + child_obj_xml = '' + elif meta_type == 'InformationRegister': + reg_key = f'InformationRegister.{obj_name}' + cols = list(register_columns.get(reg_key, {}).keys()) or ['\u0417\u0430\u0433\u043b\u0443\u0448\u043a\u0430'] + parts = [] + for i, col in enumerate(cols): + u = new_uuid() + if i == 0: + parts.append(f"""\t\t\t +\t\t\t\t +\t\t\t\t\t{col} +\t\t\t\t\t +\t\t\t\t\txs:string10Variable +\t\t\t\t\tfalsefalse +\t\t\t\t\tfalsefalse +\t\t\t\t\t +\t\t\t\t\tfalseDontCheck +\t\t\t\t\tItems +\t\t\t\t\tAutoAutoAuto +\t\t\t\t\tfalsetruefalse +\t\t\t\t\tDontIndexUseUse +\t\t\t\t +\t\t\t""") + else: + parts.append(f"""\t\t\t +\t\t\t\t +\t\t\t\t\t{col} +\t\t\t\t\t +\t\t\t\t\txs:string10Variable +\t\t\t\t\tfalsefalse +\t\t\t\t\tfalsefalse +\t\t\t\t\t +\t\t\t\t\tfalseDontCheck +\t\t\t\t\tItems +\t\t\t\t\tAutoAutoAuto +\t\t\t\t\tDontIndexUseUse +\t\t\t\t +\t\t\t""") + child_obj_xml = '\n\t\t\n' + '\n'.join(parts) + '\n\t\t' + elif meta_type in ('AccumulationRegister', 'AccountingRegister', 'CalculationRegister'): + reg_key = f'{meta_type}.{obj_name}' + cols = list(register_columns.get(reg_key, {}).keys()) + parts = [] + # Required stub Resource + parts.append(f"""\t\t\t +\t\t\t\t +\t\t\t\t\t\u0417\u0430\u0433\u043b\u0443\u0448\u043a\u0430 +\t\t\t\t\t +\t\t\t\t\txs:decimal152Any +\t\t\t\t\tfalsefalse +\t\t\t\t\tfalsefalse +\t\t\t\t\tDontCheck +\t\t\t\t\tItems +\t\t\t\t\tAutoAutoAuto +\t\t\t\t\tUse +\t\t\t\t +\t\t\t""") + # Form-referenced columns as Dimensions + for col in cols: + parts.append(f"""\t\t\t +\t\t\t\t +\t\t\t\t\t{col} +\t\t\t\t\t +\t\t\t\t\txs:string10Variable +\t\t\t\t\tfalsefalse +\t\t\t\t\tfalsefalse +\t\t\t\t\tDontCheck +\t\t\t\t\tItems +\t\t\t\t\tAutoAutoAuto +\t\t\t\t\tUse +\t\t\t\t +\t\t\t""") + child_obj_xml = '\n\t\t\n' + '\n'.join(parts) + '\n\t\t' + else: + child_obj_xml = '\n\t\t' + + obj_xml = f""" + +\t<{tag} uuid="{obj_uuid}">{internal_xml} +\t\t +{props_xml} +\t\t{child_obj_xml} +\t + +""" + write_bom(os.path.join(obj_dir, f'{obj_name}.xml'), obj_xml) + + print(f'Generated stub configuration with {len(type_map)} metadata types') + if register_columns: + print('WARNING: Register column categories (Dimension/Resource/Attribute) are guessed. Form field bindings may not survive round-trip through a real database.') + + # Create infobase + print(f'Creating infobase: {temp_base}') + result = subprocess.run( + [args.V8Path, 'CREATEINFOBASE', f'File={temp_base}', '/DisableStartupDialogs'], + capture_output=True, text=True, + ) + if result.returncode != 0: + print(f'Failed to create infobase (code: {result.returncode})', file=sys.stderr) + sys.exit(1) + + if has_ref_types: + cfg_dir = os.path.join(temp_base, 'cfg') + # LoadConfigFromFiles + print('Loading configuration from files...') + result = subprocess.run( + [args.V8Path, 'DESIGNER', f'/F{temp_base}', '/LoadConfigFromFiles', cfg_dir, '/DisableStartupDialogs'], + capture_output=True, text=True, + ) + if result.returncode != 0: + print(f'Failed to load config (code: {result.returncode})', file=sys.stderr) + sys.exit(1) + + # UpdateDBCfg + print('Updating database configuration...') + update_log = os.path.join(tempfile.gettempdir(), 'stub_update_log.txt') + result = subprocess.run( + [args.V8Path, 'DESIGNER', f'/F{temp_base}', '/UpdateDBCfg', '/Out', update_log, '/DisableStartupDialogs'], + capture_output=True, text=True, + ) + if result.returncode != 0: + if os.path.isfile(update_log): + try: + with open(update_log, 'r', encoding='utf-8-sig') as f: + print(f.read()) + except Exception: + pass + print(f'Failed to update DB config (code: {result.returncode})', file=sys.stderr) + sys.exit(1) + + # Cleanup cfg dir + import shutil + shutil.rmtree(cfg_dir, ignore_errors=True) + + print(f'[OK] Stub database created: {temp_base}') + print(temp_base) + + +if __name__ == '__main__': + main() diff --git a/.codex/skills/epf-dump/SKILL.md b/.codex/skills/epf-dump/SKILL.md new file mode 100644 index 00000000..722871c5 --- /dev/null +++ b/.codex/skills/epf-dump/SKILL.md @@ -0,0 +1,69 @@ +--- +name: epf-dump +description: Разобрать EPF-файл обработки 1С (EPF/ERF) в XML-исходники. Используй когда пользователь просит разобрать, декомпилировать обработку, получить исходники из EPF/ERF файла +argument-hint: +allowed-tools: + - Bash + - Read + - Glob + - Grep +--- + +# /epf-dump — Разборка обработки + +## Usage + +``` +/epf-dump [OutDir] +``` + +| Параметр | Обязательный | По умолчанию | Описание | +|----------|:------------:|--------------|-------------------------------------| +| EpfFile | да | — | Путь к EPF-файлу | +| OutDir | нет | `src` | Каталог для выгрузки исходников | + +## Параметры подключения (обязательно) + +Для разборки EPF/ERF требуется информационная база с конфигурацией. Без базы ссылочные типы безвозвратно теряются. + +1. Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` и разреши базу: +2. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую +3. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` +4. Если не указал — сопоставь текущую ветку Git с `databases[].branches` +5. Если ветка не совпала — используй `default` +6. Если `.v8-project.json` нет или база не найдена — **сообщи пользователю об ошибке**. Для dump база обязательна: в пустой базе ссылочные типы (CatalogRef, DocumentRef и т.д.) безвозвратно сбрасываются в строки. Предложи указать базу или зарегистрировать через `/db-list add`. + +Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1` +Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. + +## Команда + +```powershell +powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/epf-dump/scripts/epf-dump.ps1" <параметры> +``` + +### Параметры скрипта + +| Параметр | Обязательный | Описание | +|----------|:------------:|----------| +| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) | +| `-InfoBasePath <путь>` | * | Файловая база | +| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) | +| `-InfoBaseRef <имя>` | * | Имя базы на сервере | +| `-UserName <имя>` | нет | Имя пользователя | +| `-Password <пароль>` | нет | Пароль | +| `-InputFile <путь>` | да | Путь к EPF/ERF-файлу | +| `-OutputDir <путь>` | да | Каталог для выгрузки исходников | +| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` | + +> `*` — обязательно хотя бы одно подключение. Без базы скрипт завершится с ошибкой (dump в пустой базе безвозвратно теряет ссылочные типы) + +## Примеры + +```powershell +# Разборка обработки (файловая база) +powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/epf-dump/scripts/epf-dump.ps1" -InfoBasePath "C:\Bases\MyDB" -InputFile "build/МояОбработка.epf" -OutputDir "src" + +# Серверная база +powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/epf-dump/scripts/epf-dump.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -InputFile "build/МояОбработка.epf" -OutputDir "src" +``` diff --git a/.codex/skills/epf-dump/scripts/epf-dump.ps1 b/.codex/skills/epf-dump/scripts/epf-dump.ps1 new file mode 100644 index 00000000..a15a5e2f --- /dev/null +++ b/.codex/skills/epf-dump/scripts/epf-dump.ps1 @@ -0,0 +1,167 @@ +# epf-dump v1.0 — Dump external data processor or report (EPF/ERF) to XML sources +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +<# +.SYNOPSIS + Разборка внешней обработки/отчёта 1С в XML-исходники + +.DESCRIPTION + Разбирает EPF/ERF-файл во XML-исходники с помощью платформы 1С. + Общий скрипт для epf-dump и erf-dump. + +.PARAMETER V8Path + Путь к каталогу bin платформы или к 1cv8.exe + +.PARAMETER InfoBasePath + Путь к файловой информационной базе + +.PARAMETER InfoBaseServer + Сервер 1С (для серверной базы) + +.PARAMETER InfoBaseRef + Имя базы на сервере + +.PARAMETER UserName + Имя пользователя 1С + +.PARAMETER Password + Пароль пользователя + +.PARAMETER InputFile + Путь к EPF/ERF-файлу + +.PARAMETER OutputDir + Каталог для выгрузки исходников + +.PARAMETER Format + Формат выгрузки: Hierarchical или Plain (по умолчанию Hierarchical) + +.EXAMPLE + .\epf-dump.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "build\МояОбработка.epf" -OutputDir "src" + +.EXAMPLE + .\epf-dump.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "build\МойОтчёт.erf" -OutputDir "src" +#> + +[CmdletBinding()] +param( + [Parameter(Mandatory=$false)] + [string]$V8Path, + + [Parameter(Mandatory=$false)] + [string]$InfoBasePath, + + [Parameter(Mandatory=$false)] + [string]$InfoBaseServer, + + [Parameter(Mandatory=$false)] + [string]$InfoBaseRef, + + [Parameter(Mandatory=$false)] + [string]$UserName, + + [Parameter(Mandatory=$false)] + [string]$Password, + + [Parameter(Mandatory=$true)] + [string]$InputFile, + + [Parameter(Mandatory=$true)] + [string]$OutputDir, + + [Parameter(Mandatory=$false)] + [ValidateSet("Hierarchical", "Plain")] + [string]$Format = "Hierarchical" +) + +$OutputEncoding = [System.Text.Encoding]::UTF8 +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- Resolve V8Path --- +if (-not $V8Path) { + $found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1 + if ($found) { + $V8Path = $found.FullName + } else { + Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red + exit 1 + } +} elseif (Test-Path $V8Path -PathType Container) { + $V8Path = Join-Path $V8Path "1cv8.exe" +} + +if (-not (Test-Path $V8Path)) { + Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red + exit 1 +} + +# --- Validate database connection --- +if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) { + Write-Host "Error: database connection required. Specify -InfoBasePath or -InfoBaseServer/-InfoBaseRef" -ForegroundColor Red + Write-Host "Dump in an empty database loses reference types (CatalogRef, DocumentRef, etc.) irreversibly." -ForegroundColor Yellow + exit 1 +} + +# --- Validate input file --- +if (-not (Test-Path $InputFile)) { + Write-Host "Error: input file not found: $InputFile" -ForegroundColor Red + exit 1 +} + +# --- Ensure output directory exists --- +if (-not (Test-Path $OutputDir)) { + New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null +} + +# --- Temp dir --- +$tempDir = Join-Path $env:TEMP "epf_dump_$(Get-Random)" +New-Item -ItemType Directory -Path $tempDir -Force | Out-Null + +try { + # --- Build arguments --- + $arguments = @("DESIGNER") + + if ($InfoBaseServer -and $InfoBaseRef) { + $arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`"" + } else { + $arguments += "/F", "`"$InfoBasePath`"" + } + + if ($UserName) { $arguments += "/N`"$UserName`"" } + if ($Password) { $arguments += "/P`"$Password`"" } + + $arguments += "/DumpExternalDataProcessorOrReportToFiles", "`"$OutputDir`"", "`"$InputFile`"" + $arguments += "-Format", $Format + + # --- Output --- + $outFile = Join-Path $tempDir "dump_log.txt" + $arguments += "/Out", "`"$outFile`"" + $arguments += "/DisableStartupDialogs" + + # --- Execute --- + Write-Host "Running: 1cv8.exe $($arguments -join ' ')" + $process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru + $exitCode = $process.ExitCode + + # --- Result --- + if ($exitCode -eq 0) { + Write-Host "Dump completed successfully to: $OutputDir" -ForegroundColor Green + } else { + Write-Host "Error dumping (code: $exitCode)" -ForegroundColor Red + } + + if (Test-Path $outFile) { + $logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue + if ($logContent) { + Write-Host "--- Log ---" + Write-Host $logContent + Write-Host "--- End ---" + } + } + + exit $exitCode + +} finally { + if (Test-Path $tempDir) { + Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue + } +} diff --git a/.codex/skills/epf-dump/scripts/epf-dump.py b/.codex/skills/epf-dump/scripts/epf-dump.py new file mode 100644 index 00000000..ab1c8ea0 --- /dev/null +++ b/.codex/skills/epf-dump/scripts/epf-dump.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python3 +# epf-dump v1.0 — Dump external data processor or report (EPF/ERF) to XML sources +# 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(): + sys.stdout.reconfigure(encoding="utf-8") + sys.stderr.reconfigure(encoding="utf-8") + parser = argparse.ArgumentParser( + description="Dump external data processor or report (EPF/ERF) to XML sources", + 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("-InputFile", required=True, help="Path to EPF/ERF file") + parser.add_argument("-OutputDir", required=True, help="Directory for dumped XML sources") + 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 database connection --- + if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef): + print("Error: database connection required. Specify -InfoBasePath or -InfoBaseServer/-InfoBaseRef", file=sys.stderr) + print("Dump in an empty database loses reference types (CatalogRef, DocumentRef, etc.) irreversibly.") + 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) + + # --- Ensure output directory exists --- + if not os.path.exists(args.OutputDir): + os.makedirs(args.OutputDir, exist_ok=True) + + # --- Temp dir --- + temp_dir = os.path.join(tempfile.gettempdir(), f"epf_dump_{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", args.InfoBasePath] + + if args.UserName: + arguments.append(f"/N{args.UserName}") + if args.Password: + arguments.append(f"/P{args.Password}") + + arguments += ["/DumpExternalDataProcessorOrReportToFiles", args.OutputDir, args.InputFile] + arguments += ["-Format", args.Format] + + # --- Output --- + out_file = os.path.join(temp_dir, "dump_log.txt") + arguments += ["/Out", 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"Dump completed successfully to: {args.OutputDir}") + else: + print(f"Error dumping (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/.codex/skills/epf-init/SKILL.md b/.codex/skills/epf-init/SKILL.md new file mode 100644 index 00000000..11e7c077 --- /dev/null +++ b/.codex/skills/epf-init/SKILL.md @@ -0,0 +1,41 @@ +--- +name: epf-init +description: Создать пустую внешнюю обработку 1С (scaffold XML-исходников). Используй когда нужно создать новую внешнюю обработку с нуля +argument-hint: [Synonym] +allowed-tools: + - Bash + - Read + - Write + - Edit + - Glob + - Grep +--- + +# /epf-init — Создание новой обработки + +Генерирует минимальный набор XML-исходников для внешней обработки 1С: корневой файл метаданных и каталог обработки. + +## Usage + +``` +/epf-init [Synonym] [SrcDir] +``` + +| Параметр | Обязательный | По умолчанию | Описание | +|-----------|:------------:|--------------|-------------------------------------| +| Name | да | — | Имя обработки (латиница/кириллица) | +| Synonym | нет | = Name | Синоним (отображаемое имя) | +| SrcDir | нет | `src` | Каталог исходников относительно CWD | + +## Команда + +```powershell +powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/epf-init/scripts/init.ps1" -Name "" [-Synonym ""] [-SrcDir ""] +``` + +## Дальнейшие шаги + +- Добавить форму: `/form-add` +- Добавить макет: `/template-add` +- Добавить справку: `/help-add` +- Собрать EPF: `/epf-build` diff --git a/.codex/skills/epf-init/scripts/init.ps1 b/.codex/skills/epf-init/scripts/init.ps1 new file mode 100644 index 00000000..966caa3a --- /dev/null +++ b/.codex/skills/epf-init/scripts/init.ps1 @@ -0,0 +1,90 @@ +# epf-init v1.1 — Init 1C external data processor scaffold +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( + [Parameter(Mandatory)] + [string]$Name, + + [string]$Synonym = $Name, + + [string]$SrcDir = "src" +) + +$ErrorActionPreference = "Stop" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 +[Console]::InputEncoding = [System.Text.Encoding]::UTF8 + +$uuid1 = [guid]::NewGuid().ToString() +$uuid2 = [guid]::NewGuid().ToString() +$uuid3 = [guid]::NewGuid().ToString() +$uuid4 = [guid]::NewGuid().ToString() + +$xml = @" + + + + + + c3831ec8-d8d5-4f93-8a22-f9bfae07327f + $uuid2 + + + $uuid3 + $uuid4 + + + + $Name + + + ru + $Synonym + + + + + + + + + +"@ + +$rootFile = Join-Path $SrcDir "$Name.xml" +$processorDir = Join-Path $SrcDir $Name + +if (Test-Path $rootFile) { + Write-Error "Файл уже существует: $rootFile" + exit 1 +} + +if (-not (Test-Path $SrcDir)) { + New-Item -ItemType Directory -Path $SrcDir -Force | Out-Null +} +$extDir = Join-Path $processorDir "Ext" +New-Item -ItemType Directory -Path $extDir -Force | Out-Null + +$enc = New-Object System.Text.UTF8Encoding($true) +[System.IO.File]::WriteAllText((Resolve-Path $SrcDir | Join-Path -ChildPath "$Name.xml"), $xml, $enc) + +# --- Модуль объекта --- + +$moduleBsl = @" +#Область ОписаниеПеременных + +#КонецОбласти + +#Область ПрограммныйИнтерфейс + +#КонецОбласти + +#Область СлужебныеПроцедурыИФункции + +#КонецОбласти +"@ + +$modulePath = Join-Path $extDir "ObjectModule.bsl" +[System.IO.File]::WriteAllText($modulePath, $moduleBsl, $enc) + +Write-Host "[OK] Создана обработка: $rootFile" +Write-Host " Каталог: $processorDir" +Write-Host " Модуль: $modulePath" diff --git a/.codex/skills/epf-init/scripts/init.py b/.codex/skills/epf-init/scripts/init.py new file mode 100644 index 00000000..3f532727 --- /dev/null +++ b/.codex/skills/epf-init/scripts/init.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +# epf-init v1.1 — Init 1C external data processor scaffold +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +"""Generates minimal XML source files for a 1C external data processor.""" +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(): + sys.stdout.reconfigure(encoding="utf-8") + sys.stderr.reconfigure(encoding="utf-8") + parser = argparse.ArgumentParser(description='Init 1C external data processor scaffold', allow_abbrev=False) + parser.add_argument('-Name', dest='Name', required=True) + parser.add_argument('-Synonym', dest='Synonym', default=None) + parser.add_argument('-SrcDir', dest='SrcDir', default='src') + args = parser.parse_args() + + name = args.Name + synonym = args.Synonym if args.Synonym else name + src_dir = args.SrcDir + + uuid1 = new_uuid() + uuid2 = new_uuid() + uuid3 = new_uuid() + uuid4 = new_uuid() + + xml = f''' + +\t +\t\t +\t\t\t +\t\t\t\tc3831ec8-d8d5-4f93-8a22-f9bfae07327f +\t\t\t\t{uuid2} +\t\t\t +\t\t\t +\t\t\t\t{uuid3} +\t\t\t\t{uuid4} +\t\t\t +\t\t +\t\t +\t\t\t{esc_xml(name)} +\t\t\t +\t\t\t\t +\t\t\t\t\tru +\t\t\t\t\t{esc_xml(synonym)} +\t\t\t\t +\t\t\t +\t\t\t +\t\t\t +\t\t\t +\t\t +\t\t +\t +''' + + root_file = os.path.join(src_dir, f"{name}.xml") + processor_dir = os.path.join(src_dir, name) + + if os.path.exists(root_file): + print(f"Файл уже существует: {root_file}", file=sys.stderr) + sys.exit(1) + + os.makedirs(src_dir, exist_ok=True) + ext_dir = os.path.join(processor_dir, "Ext") + os.makedirs(ext_dir, exist_ok=True) + + write_utf8_bom(os.path.join(os.path.abspath(src_dir), f"{name}.xml"), xml) + + # --- Модуль объекта --- + module_bsl = """\ +#Область ОписаниеПеременных + +#КонецОбласти + +#Область ПрограммныйИнтерфейс + +#КонецОбласти + +#Область СлужебныеПроцедурыИФункции + +#КонецОбласти""" + + module_path = os.path.join(ext_dir, "ObjectModule.bsl") + write_utf8_bom(module_path, module_bsl) + + print(f"[OK] Создана обработка: {root_file}") + print(f" Каталог: {processor_dir}") + print(f" Модуль: {module_path}") + +if __name__ == '__main__': + main() diff --git a/.codex/skills/epf-validate/SKILL.md b/.codex/skills/epf-validate/SKILL.md new file mode 100644 index 00000000..bd141f6c --- /dev/null +++ b/.codex/skills/epf-validate/SKILL.md @@ -0,0 +1,30 @@ +--- +name: epf-validate +description: Валидация внешней обработки 1С (EPF). Используй после создания или модификации обработки для проверки корректности +argument-hint: [-Detailed] [-MaxErrors 30] +allowed-tools: + - Bash + - Read + - Glob +--- + +# /epf-validate — валидация внешней обработки (EPF) + +Проверяет структурную корректность XML-исходников внешней обработки: корневую структуру, InternalInfo, свойства, ChildObjects, реквизиты, табличные части, уникальность имён, наличие файлов форм и макетов. Также работает для внешних отчётов (ERF). + +## Параметры + +| Параметр | Обяз. | Умолч. | Описание | +|------------|:-----:|---------|-------------------------------------------------| +| ObjectPath | да | — | Путь к корневому XML или каталогу обработки | +| Detailed | нет | — | Подробный вывод (все проверки, включая успешные) | +| MaxErrors | нет | 30 | Остановиться после N ошибок | +| OutFile | нет | — | Записать результат в файл (UTF-8 BOM) | + +## Команда + +```powershell +powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/epf-validate/scripts/epf-validate.ps1" -ObjectPath "src/МояОбработка" +powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/epf-validate/scripts/epf-validate.ps1" -ObjectPath "src/МояОбработка/МояОбработка.xml" +``` + diff --git a/.codex/skills/epf-validate/scripts/epf-validate.ps1 b/.codex/skills/epf-validate/scripts/epf-validate.ps1 new file mode 100644 index 00000000..000219c5 --- /dev/null +++ b/.codex/skills/epf-validate/scripts/epf-validate.ps1 @@ -0,0 +1,842 @@ +# epf-validate v1.2 — Validate 1C external data processor / report structure +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +# Works for both EPF (ExternalDataProcessor) and ERF (ExternalReport) — auto-detects +param( + [Parameter(Mandatory)] + [Alias('Path')] + [string]$ObjectPath, + + [switch]$Detailed, + + [int]$MaxErrors = 30, + + [string]$OutFile +) + +$ErrorActionPreference = "Stop" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- Resolve path --- + +if (-not [System.IO.Path]::IsPathRooted($ObjectPath)) { + $ObjectPath = Join-Path (Get-Location).Path $ObjectPath +} + +if (Test-Path $ObjectPath -PathType Container) { + $dirName = Split-Path $ObjectPath -Leaf + $candidate = Join-Path $ObjectPath "$dirName.xml" + $sibling = Join-Path (Split-Path $ObjectPath) "$dirName.xml" + if (Test-Path $candidate) { + $ObjectPath = $candidate + } elseif (Test-Path $sibling) { + $ObjectPath = $sibling + } else { + $xmlFiles = @(Get-ChildItem $ObjectPath -Filter "*.xml" -File | Select-Object -First 1) + if ($xmlFiles.Count -gt 0) { + $ObjectPath = $xmlFiles[0].FullName + } else { + Write-Host "[ERROR] No XML file found in directory: $ObjectPath" + exit 1 + } + } +} + +# File not found — check Dir/Name/Name.xml → Dir/Name.xml +if (-not (Test-Path $ObjectPath)) { + $fileName = [System.IO.Path]::GetFileNameWithoutExtension($ObjectPath) + $parentDir = Split-Path $ObjectPath + $parentDirName = Split-Path $parentDir -Leaf + if ($fileName -eq $parentDirName) { + $candidate = Join-Path (Split-Path $parentDir) "$fileName.xml" + if (Test-Path $candidate) { $ObjectPath = $candidate } + } +} +if (-not (Test-Path $ObjectPath)) { + Write-Host "[ERROR] File not found: $ObjectPath" + exit 1 +} + +$resolvedPath = (Resolve-Path $ObjectPath).Path +$srcDir = Split-Path $resolvedPath -Parent + +# --- Output infrastructure --- + +$script:errors = 0 +$script:warnings = 0 +$script:okCount = 0 +$script:stopped = $false +$script:output = New-Object System.Text.StringBuilder 8192 + +function Out-Line { + param([string]$msg) + $script:output.AppendLine($msg) | Out-Null +} + +function Report-OK { + param([string]$msg) + $script:okCount++ + if ($Detailed) { Out-Line "[OK] $msg" } +} + +function Report-Error { + param([string]$msg) + $script:errors++ + Out-Line "[ERROR] $msg" + if ($script:errors -ge $MaxErrors) { + $script:stopped = $true + } +} + +function Report-Warn { + param([string]$msg) + $script:warnings++ + Out-Line "[WARN] $msg" +} + +$finalize = { + $checks = $script:okCount + $script:errors + $script:warnings + if ($script:errors -eq 0 -and $script:warnings -eq 0 -and -not $Detailed) { + $result = "=== Validation OK: $shortType.$objName ($checks checks) ===" + } else { + Out-Line "" + Out-Line "=== Result: $($script:errors) errors, $($script:warnings) warnings ($checks checks) ===" + $result = $script:output.ToString() + } + Write-Host $result + + if ($OutFile) { + $utf8Bom = New-Object System.Text.UTF8Encoding $true + [System.IO.File]::WriteAllText($OutFile, $result, $utf8Bom) + Write-Host "Written to: $OutFile" + } +} + +# --- Reference tables --- + +$guidPattern = '^[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}$' +$identPattern = '^[A-Za-z\u0410-\u042F\u0401\u0430-\u044F\u0451_][A-Za-z0-9\u0410-\u042F\u0401\u0430-\u044F\u0451_]*$' + +$classIds = @{ + "ExternalDataProcessor" = "c3831ec8-d8d5-4f93-8a22-f9bfae07327f" + "ExternalReport" = "e41aff26-25cf-4bb6-b6c1-3f478a75f374" +} + +$allowedChildTypes = @("Attribute","TabularSection","Form","Template","Command") + +# Expected order of child types in ChildObjects +$childTypeOrder = @{ + "Attribute" = 0 + "TabularSection" = 1 + "Form" = 2 + "Template" = 3 + "Command" = 4 +} + +$validPropertyValues = @{ + "FillChecking" = @("DontCheck","ShowError","ShowWarning") + "Indexing" = @("DontIndex","Index","IndexWithAdditionalOrder") +} + +# --- 1. Parse XML --- + +Out-Line "" + +$xmlDoc = $null +try { + $xmlDoc = New-Object System.Xml.XmlDocument + $xmlDoc.PreserveWhitespace = $false + $xmlDoc.Load($resolvedPath) +} catch { + Out-Line "=== Validation: (parse failed) ===" + Out-Line "" + Report-Error "1. XML parse failed: $($_.Exception.Message)" + & $finalize + exit 1 +} + +# --- Register namespaces --- + +$ns = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable) +$ns.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") +$ns.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core") +$ns.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable") +$ns.AddNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance") +$ns.AddNamespace("xs", "http://www.w3.org/2001/XMLSchema") +$ns.AddNamespace("app", "http://v8.1c.ru/8.2/managed-application/core") + +$root = $xmlDoc.DocumentElement + +# --- Check 1: Root structure --- + +$check1Ok = $true + +if ($root.LocalName -ne "MetaDataObject") { + Report-Error "1. Root element is '$($root.LocalName)', expected 'MetaDataObject'" + & $finalize + exit 1 +} + +$expectedNs = "http://v8.1c.ru/8.3/MDClasses" +if ($root.NamespaceURI -ne $expectedNs) { + Report-Error "1. Root namespace is '$($root.NamespaceURI)', expected '$expectedNs'" + $check1Ok = $false +} + +$version = $root.GetAttribute("version") +if (-not $version) { + Report-Warn "1. Missing version attribute on MetaDataObject" +} elseif ($version -ne "2.17" -and $version -ne "2.20" -and $version -ne "2.21") { + Report-Warn "1. Unusual version '$version' (expected 2.17, 2.20 or 2.21)" +} + +# Detect type: ExternalDataProcessor or ExternalReport +$typeNode = $null +$mdType = "" +$childElements = @() +foreach ($child in $root.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.NamespaceURI -eq $expectedNs) { + $childElements += $child + } +} + +if ($childElements.Count -eq 0) { + Report-Error "1. No metadata type element found inside MetaDataObject" + & $finalize + exit 1 +} elseif ($childElements.Count -gt 1) { + Report-Error "1. Multiple type elements found: $($childElements | ForEach-Object { $_.LocalName })" + $check1Ok = $false +} + +$typeNode = $childElements[0] +$mdType = $typeNode.LocalName + +if ($mdType -ne "ExternalDataProcessor" -and $mdType -ne "ExternalReport") { + Report-Error "1. Unexpected type '$mdType' (expected ExternalDataProcessor or ExternalReport)" + & $finalize + exit 1 +} + +$typeUuid = $typeNode.GetAttribute("uuid") +if (-not $typeUuid) { + Report-Error "1. Missing uuid on <$mdType>" + $check1Ok = $false +} elseif ($typeUuid -notmatch $guidPattern) { + Report-Error "1. Invalid uuid '$typeUuid' on <$mdType>" + $check1Ok = $false +} + +# Get object name +$propsNode = $typeNode.SelectSingleNode("md:Properties", $ns) +$nameNode = if ($propsNode) { $propsNode.SelectSingleNode("md:Name", $ns) } else { $null } +$objName = if ($nameNode -and $nameNode.InnerText) { $nameNode.InnerText } else { "(unknown)" } + +$shortType = if ($mdType -eq "ExternalDataProcessor") { "EPF" } else { "ERF" } +$script:output.Insert(0, "=== Validation: $shortType.$objName ===$([Environment]::NewLine)") | Out-Null + +if ($check1Ok) { + Report-OK "1. Root structure: MetaDataObject/$mdType, version $version" +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 2: InternalInfo --- + +$internalInfo = $typeNode.SelectSingleNode("md:InternalInfo", $ns) + +if (-not $internalInfo) { + Report-Error "2. InternalInfo block missing" +} else { + $check2Ok = $true + + # ContainedObject / ClassId + $containedObj = $internalInfo.SelectSingleNode("xr:ContainedObject", $ns) + if (-not $containedObj) { + Report-Error "2. InternalInfo: missing xr:ContainedObject" + $check2Ok = $false + } else { + $classIdNode = $containedObj.SelectSingleNode("xr:ClassId", $ns) + $objectIdNode = $containedObj.SelectSingleNode("xr:ObjectId", $ns) + + $expectedClassId = $classIds[$mdType] + if (-not $classIdNode -or -not $classIdNode.InnerText) { + Report-Error "2. Missing ClassId in ContainedObject" + $check2Ok = $false + } elseif ($classIdNode.InnerText -ne $expectedClassId) { + Report-Error "2. ClassId is '$($classIdNode.InnerText)', expected '$expectedClassId' for $mdType" + $check2Ok = $false + } + + if ($objectIdNode -and $objectIdNode.InnerText -notmatch $guidPattern) { + Report-Error "2. Invalid ObjectId UUID" + $check2Ok = $false + } + } + + # GeneratedType — expect exactly 1 with category "Object" + $genTypes = $internalInfo.SelectNodes("xr:GeneratedType", $ns) + if ($genTypes.Count -eq 0) { + Report-Error "2. No GeneratedType entries found" + $check2Ok = $false + } else { + foreach ($gt in $genTypes) { + $gtName = $gt.GetAttribute("name") + $gtCategory = $gt.GetAttribute("category") + + if ($gtCategory -ne "Object") { + Report-Warn "2. Unexpected GeneratedType category '$gtCategory' (expected 'Object')" + } + + # Name format: ExternalDataProcessorObject.Name or ExternalReportObject.Name + $expectedPrefix = "${mdType}Object." + if ($gtName -and $objName -ne "(unknown)" -and -not $gtName.StartsWith($expectedPrefix)) { + Report-Warn "2. GeneratedType name '$gtName' does not start with '$expectedPrefix'" + } + + $typeId = $gt.SelectSingleNode("xr:TypeId", $ns) + $valueId = $gt.SelectSingleNode("xr:ValueId", $ns) + if ($typeId -and $typeId.InnerText -notmatch $guidPattern) { + Report-Error "2. Invalid TypeId UUID in GeneratedType" + $check2Ok = $false + } + if ($valueId -and $valueId.InnerText -notmatch $guidPattern) { + Report-Error "2. Invalid ValueId UUID in GeneratedType" + $check2Ok = $false + } + } + } + + if ($check2Ok) { + Report-OK "2. InternalInfo: ClassId correct, $($genTypes.Count) GeneratedType" + } +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 3: Properties --- + +if (-not $propsNode) { + Report-Error "3. Properties block missing" +} else { + $check3Ok = $true + + # Name + if (-not $nameNode -or -not $nameNode.InnerText) { + Report-Error "3. Properties: Name is missing or empty" + $check3Ok = $false + } else { + $nameVal = $nameNode.InnerText + if ($nameVal -notmatch $identPattern) { + Report-Error "3. Properties: Name '$nameVal' is not a valid 1C identifier" + $check3Ok = $false + } + if ($nameVal.Length -gt 80) { + Report-Warn "3. Properties: Name '$nameVal' exceeds 80 characters ($($nameVal.Length))" + } + } + + # Synonym + $synNode = $propsNode.SelectSingleNode("md:Synonym", $ns) + $synPresent = $false + if ($synNode) { + $synItem = $synNode.SelectSingleNode("v8:item", $ns) + if ($synItem) { + $synContent = $synItem.SelectSingleNode("v8:content", $ns) + if ($synContent -and $synContent.InnerText) { + $synPresent = $true + } + } + } + + # DefaultForm cross-reference (collected now, checked after ChildObjects) + $defaultFormNode = $propsNode.SelectSingleNode("md:DefaultForm", $ns) + $defaultFormVal = if ($defaultFormNode -and $defaultFormNode.InnerText.Trim()) { $defaultFormNode.InnerText.Trim() } else { "" } + + # AuxiliaryForm cross-reference + $auxFormNode = $propsNode.SelectSingleNode("md:AuxiliaryForm", $ns) + $auxFormVal = if ($auxFormNode -and $auxFormNode.InnerText.Trim()) { $auxFormNode.InnerText.Trim() } else { "" } + + # ERF-specific: MainDataCompositionSchema + $mainDCSVal = "" + if ($mdType -eq "ExternalReport") { + $mainDCSNode = $propsNode.SelectSingleNode("md:MainDataCompositionSchema", $ns) + $mainDCSVal = if ($mainDCSNode -and $mainDCSNode.InnerText.Trim()) { $mainDCSNode.InnerText.Trim() } else { "" } + } + + if ($check3Ok) { + $synInfo = if ($synPresent) { "Synonym present" } else { "no Synonym" } + $extras = "" + if ($defaultFormVal) { $extras += ", DefaultForm set" } + if ($mainDCSVal) { $extras += ", MainDCS set" } + Report-OK "3. Properties: Name=`"$objName`", $synInfo$extras" + } +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 4: ChildObjects — allowed types and ordering --- + +$childObjNode = $typeNode.SelectSingleNode("md:ChildObjects", $ns) +$formNames = @() +$templateNames = @() +$allChildNames = @{} + +if ($childObjNode) { + $check4Ok = $true + $childCounts = @{} + $lastOrder = -1 + $orderOk = $true + + foreach ($child in $childObjNode.ChildNodes) { + if ($child.NodeType -ne 'Element') { continue } + $childTag = $child.LocalName + + if ($allowedChildTypes -notcontains $childTag) { + Report-Error "4. ChildObjects: disallowed element '$childTag'" + $check4Ok = $false + continue + } + + if (-not $childCounts.ContainsKey($childTag)) { + $childCounts[$childTag] = 0 + } + $childCounts[$childTag]++ + + # Check ordering + $thisOrder = $childTypeOrder[$childTag] + if ($thisOrder -lt $lastOrder -and $orderOk) { + Report-Warn "4. ChildObjects: '$childTag' appears after higher-order elements (expected: Attribute, TabularSection, Form, Template, Command)" + $orderOk = $false + } + $lastOrder = $thisOrder + + # Collect Form and Template names (simple text content) + if ($childTag -eq "Form") { + $formNames += $child.InnerText.Trim() + } elseif ($childTag -eq "Template") { + $templateNames += $child.InnerText.Trim() + } + } + + if ($check4Ok) { + $summary = ($childCounts.GetEnumerator() | Sort-Object { $childTypeOrder[$_.Name] } | ForEach-Object { "$($_.Name)($($_.Value))" }) -join ", " + if ($summary) { + Report-OK "4. ChildObjects: $summary" + } else { + Report-OK "4. ChildObjects: empty" + } + } +} else { + Report-OK "4. ChildObjects: absent" +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 5: DefaultForm / MainDCS cross-references --- + +$check5Ok = $true + +if ($defaultFormVal) { + # Format: ExternalDataProcessor.Name.Form.FormName or ExternalReport.Name.Form.FormName + $expectedPrefix = "$mdType.$objName.Form." + if ($defaultFormVal.StartsWith($expectedPrefix)) { + $refFormName = $defaultFormVal.Substring($expectedPrefix.Length) + if ($formNames -notcontains $refFormName) { + Report-Error "5. DefaultForm references '$refFormName', but no such Form in ChildObjects" + $check5Ok = $false + } + } else { + Report-Warn "5. DefaultForm value '$defaultFormVal' has unexpected prefix (expected '$expectedPrefix...')" + } +} + +if ($auxFormVal) { + $expectedPrefix = "$mdType.$objName.Form." + if ($auxFormVal.StartsWith($expectedPrefix)) { + $refFormName = $auxFormVal.Substring($expectedPrefix.Length) + if ($formNames -notcontains $refFormName) { + Report-Error "5. AuxiliaryForm references '$refFormName', but no such Form in ChildObjects" + $check5Ok = $false + } + } +} + +if ($mainDCSVal -and $mdType -eq "ExternalReport") { + $expectedPrefix = "ExternalReport.$objName.Template." + if ($mainDCSVal.StartsWith($expectedPrefix)) { + $refTplName = $mainDCSVal.Substring($expectedPrefix.Length) + if ($templateNames -notcontains $refTplName) { + Report-Error "5. MainDataCompositionSchema references '$refTplName', but no such Template in ChildObjects" + $check5Ok = $false + } + } else { + Report-Warn "5. MainDataCompositionSchema value '$mainDCSVal' has unexpected prefix" + } +} + +if ($check5Ok) { + $refs = @() + if ($defaultFormVal) { $refs += "DefaultForm" } + if ($auxFormVal) { $refs += "AuxiliaryForm" } + if ($mainDCSVal) { $refs += "MainDCS" } + if ($refs.Count -gt 0) { + Report-OK "5. Cross-references: $($refs -join ', ') valid" + } else { + Report-OK "5. Cross-references: none to check" + } +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 6: Attributes — UUID, Name, Type --- + +function Check-Attribute { + param( + [System.Xml.XmlNode]$node, + [string]$context + ) + + $uuid = $node.GetAttribute("uuid") + if (-not $uuid) { + Report-Error "6. $context Attribute missing uuid" + return $false + } elseif ($uuid -notmatch $guidPattern) { + Report-Error "6. $context Attribute has invalid uuid '$uuid'" + return $false + } + + $elProps = $node.SelectSingleNode("md:Properties", $ns) + if (-not $elProps) { + Report-Error "6. $context Attribute (uuid=$uuid) missing Properties" + return $false + } + + $elName = $elProps.SelectSingleNode("md:Name", $ns) + if (-not $elName -or -not $elName.InnerText) { + Report-Error "6. $context Attribute (uuid=$uuid) missing or empty Name" + return $false + } + + $nameVal = $elName.InnerText + if ($nameVal -notmatch $identPattern) { + Report-Error "6. $context Attribute '$nameVal' has invalid identifier" + return $false + } + + $typeEl = $elProps.SelectSingleNode("md:Type", $ns) + if (-not $typeEl) { + Report-Error "6. $context Attribute '$nameVal' missing Type block" + return $false + } + $v8Types = $typeEl.SelectNodes("v8:Type", $ns) + $v8TypeSets = $typeEl.SelectNodes("v8:TypeSet", $ns) + if ($v8Types.Count -eq 0 -and $v8TypeSets.Count -eq 0) { + Report-Error "6. $context Attribute '$nameVal' Type block has no v8:Type or v8:TypeSet" + return $false + } + + return $true +} + +if ($childObjNode) { + $attrs = $childObjNode.SelectNodes("md:Attribute", $ns) + $check6Ok = $true + $attrCount = 0 + + foreach ($attr in $attrs) { + if ($script:stopped) { break } + $ok = Check-Attribute -node $attr -context "" + if (-not $ok) { $check6Ok = $false } + $attrCount++ + + # Collect name for uniqueness + $ap = $attr.SelectSingleNode("md:Properties/md:Name", $ns) + if ($ap -and $ap.InnerText) { + $allChildNames["Attr:$($ap.InnerText)"] = $ap.InnerText + } + } + + if ($attrCount -gt 0) { + if ($check6Ok) { + Report-OK "6. Attributes: $attrCount checked (UUID, Name, Type)" + } + } else { + Report-OK "6. Attributes: none" + } +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 7: TabularSections --- + +if ($childObjNode) { + $tsSections = $childObjNode.SelectNodes("md:TabularSection", $ns) + if ($tsSections.Count -gt 0) { + $check7Ok = $true + $tsCount = 0 + $tsAttrTotal = 0 + + foreach ($ts in $tsSections) { + if ($script:stopped) { break } + $tsCount++ + + $tsUuid = $ts.GetAttribute("uuid") + if (-not $tsUuid -or $tsUuid -notmatch $guidPattern) { + Report-Error "7. TabularSection #${tsCount}: invalid or missing uuid" + $check7Ok = $false + } + + $tsProps = $ts.SelectSingleNode("md:Properties", $ns) + $tsNameNode = if ($tsProps) { $tsProps.SelectSingleNode("md:Name", $ns) } else { $null } + $tsName = if ($tsNameNode -and $tsNameNode.InnerText) { $tsNameNode.InnerText } else { "(unnamed)" } + + if (-not $tsNameNode -or -not $tsNameNode.InnerText) { + Report-Error "7. TabularSection #${tsCount}: missing or empty Name" + $check7Ok = $false + } elseif ($tsName -notmatch $identPattern) { + Report-Error "7. TabularSection '$tsName': invalid identifier" + $check7Ok = $false + } + + $allChildNames["TS:$tsName"] = $tsName + + # InternalInfo — expect 2 GeneratedType + $tsIntInfo = $ts.SelectSingleNode("md:InternalInfo", $ns) + if ($tsIntInfo) { + $tsGens = $tsIntInfo.SelectNodes("xr:GeneratedType", $ns) + if ($tsGens.Count -lt 2) { + Report-Warn "7. TabularSection '$tsName': expected 2 GeneratedType, found $($tsGens.Count)" + } + } + + # Inner attributes + $tsChildObj = $ts.SelectSingleNode("md:ChildObjects", $ns) + if ($tsChildObj) { + $tsAttrs = $tsChildObj.SelectNodes("md:Attribute", $ns) + $tsAttrNames = @{} + foreach ($ta in $tsAttrs) { + $taOk = Check-Attribute -node $ta -context "TabularSection '$tsName'." + if (-not $taOk) { $check7Ok = $false } + $tsAttrTotal++ + + $taProps = $ta.SelectSingleNode("md:Properties/md:Name", $ns) + if ($taProps -and $taProps.InnerText) { + if ($tsAttrNames.ContainsKey($taProps.InnerText)) { + Report-Error "7. Duplicate attribute '$($taProps.InnerText)' in TabularSection '$tsName'" + $check7Ok = $false + } else { + $tsAttrNames[$taProps.InnerText] = $true + } + } + } + } + } + + if ($check7Ok) { + Report-OK "7. TabularSections: $tsCount sections, $tsAttrTotal inner attributes" + } + } else { + Report-OK "7. TabularSections: none" + } +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 8: Name uniqueness --- + +$check8Ok = $true + +# Collect all names: attributes + tabular sections + forms + templates + commands +$allNames = @{} + +if ($childObjNode) { + $nameKinds = @( + @{ XPath = "md:Attribute"; Kind = "Attribute" }, + @{ XPath = "md:TabularSection"; Kind = "TabularSection" }, + @{ XPath = "md:Command"; Kind = "Command" } + ) + + foreach ($nk in $nameKinds) { + $nodes = $childObjNode.SelectNodes($nk.XPath, $ns) + foreach ($node in $nodes) { + $np = $node.SelectSingleNode("md:Properties/md:Name", $ns) + if ($np -and $np.InnerText) { + $nameVal = $np.InnerText + $key = "$($nk.Kind):$nameVal" + if ($allNames.ContainsKey($nameVal)) { + Report-Error "8. Duplicate name '$nameVal' ($($nk.Kind) conflicts with $($allNames[$nameVal]))" + $check8Ok = $false + } else { + $allNames[$nameVal] = $nk.Kind + } + } + } + } + + # Forms and Templates are simple text nodes + foreach ($fn in $formNames) { + if ($allNames.ContainsKey($fn)) { + Report-Error "8. Duplicate name '$fn' (Form conflicts with $($allNames[$fn]))" + $check8Ok = $false + } else { + $allNames[$fn] = "Form" + } + } + foreach ($tn in $templateNames) { + if ($allNames.ContainsKey($tn)) { + Report-Error "8. Duplicate name '$tn' (Template conflicts with $($allNames[$tn]))" + $check8Ok = $false + } else { + $allNames[$tn] = "Template" + } + } +} + +if ($check8Ok) { + Report-OK "8. Name uniqueness: $($allNames.Count) names, all unique" +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 9: File existence (forms and templates on disk) --- + +$check9Ok = $true +$filesChecked = 0 + +# Object directory: same level as root XML, named after the object +$objDir = Join-Path $srcDir $objName + +foreach ($fn in $formNames) { + # FormName.xml — form descriptor + $formMetaXml = Join-Path (Join-Path $objDir "Forms") "$fn.xml" + if (-not (Test-Path $formMetaXml)) { + Report-Error "9. Missing form descriptor: Forms/$fn.xml" + $check9Ok = $false + } else { + $filesChecked++ + } + + # FormName/Ext/Form.xml — form layout + $formXml = Join-Path (Join-Path (Join-Path (Join-Path $objDir "Forms") $fn) "Ext") "Form.xml" + if (-not (Test-Path $formXml)) { + Report-Error "9. Missing form layout: Forms/$fn/Ext/Form.xml" + $check9Ok = $false + } else { + $filesChecked++ + } +} + +foreach ($tn in $templateNames) { + # TemplateName.xml — template descriptor + $tplMetaXml = Join-Path (Join-Path $objDir "Templates") "$tn.xml" + if (-not (Test-Path $tplMetaXml)) { + Report-Error "9. Missing template descriptor: Templates/$tn.xml" + $check9Ok = $false + } else { + $filesChecked++ + } + + # TemplateName/Ext/Template.* — template content (extension varies) + $tplExtDir = Join-Path (Join-Path (Join-Path $objDir "Templates") $tn) "Ext" + if (Test-Path $tplExtDir) { + $tplFiles = @(Get-ChildItem $tplExtDir -Filter "Template.*" -File) + if ($tplFiles.Count -eq 0) { + Report-Error "9. Missing template content: Templates/$tn/Ext/Template.*" + $check9Ok = $false + } else { + $filesChecked++ + } + } else { + Report-Error "9. Missing template Ext directory: Templates/$tn/Ext/" + $check9Ok = $false + } +} + +# ObjectModule.bsl +$objModule = Join-Path (Join-Path $objDir "Ext") "ObjectModule.bsl" +if (Test-Path $objModule) { + $filesChecked++ +} + +if ($check9Ok) { + if ($filesChecked -gt 0) { + Report-OK "9. File existence: $filesChecked files verified" + } else { + Report-OK "9. File existence: no forms/templates to check" + } +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 10: Form descriptors structure --- + +$check10Ok = $true +$formsChecked = 0 + +foreach ($fn in $formNames) { + $formMetaXml = Join-Path (Join-Path $objDir "Forms") "$fn.xml" + if (-not (Test-Path $formMetaXml)) { continue } + + try { + $fDoc = New-Object System.Xml.XmlDocument + $fDoc.PreserveWhitespace = $false + $fDoc.Load($formMetaXml) + $fRoot = $fDoc.DocumentElement + + if ($fRoot.LocalName -ne "MetaDataObject") { + Report-Error "10. Form '$fn': root element is '$($fRoot.LocalName)', expected 'MetaDataObject'" + $check10Ok = $false + continue + } + + $fTypeNode = $fRoot.SelectSingleNode("md:Form", $ns) + if (-not $fTypeNode) { + Report-Error "10. Form '$fn': missing
element" + $check10Ok = $false + continue + } + + $fUuid = $fTypeNode.GetAttribute("uuid") + if (-not $fUuid -or $fUuid -notmatch $guidPattern) { + Report-Error "10. Form '$fn': invalid or missing uuid" + $check10Ok = $false + } + + $fProps = $fTypeNode.SelectSingleNode("md:Properties", $ns) + if ($fProps) { + $fName = $fProps.SelectSingleNode("md:Name", $ns) + if ($fName -and $fName.InnerText -ne $fn) { + Report-Error "10. Form '$fn': Name in descriptor is '$($fName.InnerText)', expected '$fn'" + $check10Ok = $false + } + + # FormType should be Managed + $fType = $fProps.SelectSingleNode("md:FormType", $ns) + if ($fType -and $fType.InnerText -ne "Managed") { + Report-Warn "10. Form '$fn': FormType is '$($fType.InnerText)' (expected 'Managed')" + } + } + + $formsChecked++ + } catch { + Report-Error "10. Form '$fn': XML parse error: $($_.Exception.Message)" + $check10Ok = $false + } +} + +if ($check10Ok) { + if ($formsChecked -gt 0) { + Report-OK "10. Form descriptors: $formsChecked checked" + } else { + Report-OK "10. Form descriptors: none to check" + } +} + +# --- Final output --- + +& $finalize + +if ($script:errors -gt 0) { + exit 1 +} +exit 0 diff --git a/.codex/skills/epf-validate/scripts/epf-validate.py b/.codex/skills/epf-validate/scripts/epf-validate.py new file mode 100644 index 00000000..4c854768 --- /dev/null +++ b/.codex/skills/epf-validate/scripts/epf-validate.py @@ -0,0 +1,708 @@ +#!/usr/bin/env python3 +# epf-validate v1.2 — Validate 1C external data processor / report structure +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +# Works for both EPF (ExternalDataProcessor) and ERF (ExternalReport) — auto-detects + +import argparse +import os +import re +import sys +from io import StringIO +from lxml import etree + +MD_NS = "http://v8.1c.ru/8.3/MDClasses" +V8_NS = "http://v8.1c.ru/8.1/data/core" +XR_NS = "http://v8.1c.ru/8.3/xcf/readable" +XSI_NS = "http://www.w3.org/2001/XMLSchema-instance" +XS_NS = "http://www.w3.org/2001/XMLSchema" +APP_NS = "http://v8.1c.ru/8.2/managed-application/core" + +NSMAP = {"md": MD_NS, "v8": V8_NS, "xr": XR_NS, "xsi": XSI_NS, "xs": XS_NS, "app": APP_NS} + +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_][A-Za-z0-9\u0410-\u042F\u0401\u0430-\u044F\u0451_]*$') + +CLASS_IDS = { + "ExternalDataProcessor": "c3831ec8-d8d5-4f93-8a22-f9bfae07327f", + "ExternalReport": "e41aff26-25cf-4bb6-b6c1-3f478a75f374", +} + +ALLOWED_CHILD_TYPES = {"Attribute", "TabularSection", "Form", "Template", "Command"} + +CHILD_TYPE_ORDER = { + "Attribute": 0, + "TabularSection": 1, + "Form": 2, + "Template": 3, + "Command": 4, +} + + +def localname(el): + return etree.QName(el.tag).localname + + +def main(): + sys.stdout.reconfigure(encoding="utf-8") + sys.stderr.reconfigure(encoding="utf-8") + parser = argparse.ArgumentParser(description="Validate 1C external data processor/report structure", allow_abbrev=False) + parser.add_argument("-ObjectPath", "-Path", required=True) + parser.add_argument("-Detailed", action="store_true") + parser.add_argument("-MaxErrors", type=int, default=30) + parser.add_argument("-OutFile", default=None) + args = parser.parse_args() + + max_errors = args.MaxErrors + + # --- Resolve path --- + object_path = args.ObjectPath + if not os.path.isabs(object_path): + object_path = os.path.join(os.getcwd(), object_path) + + if os.path.isdir(object_path): + dir_name = os.path.basename(object_path) + candidate = os.path.join(object_path, f"{dir_name}.xml") + sibling = os.path.join(os.path.dirname(object_path), f"{dir_name}.xml") + if os.path.isfile(candidate): + object_path = candidate + elif os.path.isfile(sibling): + object_path = sibling + else: + xml_files = [f for f in os.listdir(object_path) if f.lower().endswith(".xml")] + if xml_files: + object_path = os.path.join(object_path, xml_files[0]) + else: + print(f"[ERROR] No XML file found in directory: {object_path}") + sys.exit(1) + + if not os.path.isfile(object_path): + file_name = os.path.splitext(os.path.basename(object_path))[0] + parent_dir = os.path.dirname(object_path) + parent_dir_name = os.path.basename(parent_dir) + if file_name == parent_dir_name: + candidate = os.path.join(os.path.dirname(parent_dir), f"{file_name}.xml") + if os.path.isfile(candidate): + object_path = candidate + + if not os.path.isfile(object_path): + print(f"[ERROR] File not found: {object_path}") + sys.exit(1) + + resolved_path = os.path.abspath(object_path) + src_dir = os.path.dirname(resolved_path) + + # --- Output infrastructure --- + detailed = args.Detailed + errors = 0 + warnings = 0 + ok_count = 0 + stopped = False + output_lines = [] + + def out_line(msg): + output_lines.append(msg) + + def report_ok(msg): + nonlocal ok_count + ok_count += 1 + if detailed: + out_line(f"[OK] {msg}") + + def report_error(msg): + nonlocal errors, stopped + errors += 1 + out_line(f"[ERROR] {msg}") + if errors >= max_errors: + stopped = True + + def report_warn(msg): + nonlocal warnings + warnings += 1 + out_line(f"[WARN] {msg}") + + def finalize(): + checks = ok_count + errors + warnings + if errors == 0 and warnings == 0 and not detailed: + result = f"=== Validation OK: {short_type}.{obj_name} ({checks} checks) ===" + else: + out_line("") + out_line(f"=== Result: {errors} errors, {warnings} warnings ({checks} checks) ===") + result = "\n".join(output_lines) + print(result) + if args.OutFile: + with open(args.OutFile, "w", encoding="utf-8-sig") as fh: + fh.write(result) + print(f"Written to: {args.OutFile}") + + # --- 1. Parse XML --- + out_line("") + + try: + xml_parser = etree.XMLParser(remove_blank_text=True) + tree = etree.parse(resolved_path, xml_parser) + except Exception as e: + out_line("=== Validation: (parse failed) ===") + out_line("") + report_error(f"1. XML parse failed: {e}") + finalize() + sys.exit(1) + + root = tree.getroot() + + # --- Check 1: Root structure --- + check1_ok = True + + if localname(root) != "MetaDataObject": + report_error(f"1. Root element is '{localname(root)}', expected 'MetaDataObject'") + finalize() + sys.exit(1) + + expected_ns = MD_NS + if root.tag.split("}")[0].lstrip("{") != expected_ns: + report_error(f"1. Root namespace is '{root.tag.split('}')[0].lstrip('{')}', expected '{expected_ns}'") + check1_ok = False + + version = root.get("version", "") + if not version: + report_warn("1. Missing version attribute on MetaDataObject") + elif version not in ("2.17", "2.20", "2.21"): + report_warn(f"1. Unusual version '{version}' (expected 2.17, 2.20 or 2.21)") + + # Detect type + child_elements = [] + for child in root: + if isinstance(child.tag, str) and child.tag.startswith(f"{{{expected_ns}}}"): + child_elements.append(child) + + if not child_elements: + report_error("1. No metadata type element found inside MetaDataObject") + finalize() + sys.exit(1) + elif len(child_elements) > 1: + report_error(f"1. Multiple type elements found: {[localname(c) for c in child_elements]}") + check1_ok = False + + type_node = child_elements[0] + md_type = localname(type_node) + + if md_type not in ("ExternalDataProcessor", "ExternalReport"): + report_error(f"1. Unexpected type '{md_type}' (expected ExternalDataProcessor or ExternalReport)") + finalize() + sys.exit(1) + + type_uuid = type_node.get("uuid", "") + if not type_uuid: + report_error(f"1. Missing uuid on <{md_type}>") + check1_ok = False + elif not GUID_PATTERN.match(type_uuid): + report_error(f"1. Invalid uuid '{type_uuid}' on <{md_type}>") + check1_ok = False + + props_node = type_node.find(f"{{{MD_NS}}}Properties") + name_node = props_node.find(f"{{{MD_NS}}}Name") if props_node is not None else None + obj_name = name_node.text if name_node is not None and name_node.text else "(unknown)" + + short_type = "EPF" if md_type == "ExternalDataProcessor" else "ERF" + output_lines.insert(0, f"=== Validation: {short_type}.{obj_name} ===") + + if check1_ok: + report_ok(f"1. Root structure: MetaDataObject/{md_type}, version {version}") + + if stopped: + finalize() + sys.exit(1) + + # --- Check 2: InternalInfo --- + internal_info = type_node.find(f"{{{MD_NS}}}InternalInfo") + if internal_info is None: + report_error("2. InternalInfo block missing") + else: + check2_ok = True + + contained_obj = internal_info.find(f"{{{XR_NS}}}ContainedObject") + if contained_obj is None: + report_error("2. InternalInfo: missing xr:ContainedObject") + check2_ok = False + else: + class_id_node = contained_obj.find(f"{{{XR_NS}}}ClassId") + object_id_node = contained_obj.find(f"{{{XR_NS}}}ObjectId") + + expected_class_id = CLASS_IDS[md_type] + if class_id_node is None or not class_id_node.text: + report_error("2. Missing ClassId in ContainedObject") + check2_ok = False + elif class_id_node.text != expected_class_id: + report_error(f"2. ClassId is '{class_id_node.text}', expected '{expected_class_id}' for {md_type}") + check2_ok = False + + if object_id_node is not None and object_id_node.text and not GUID_PATTERN.match(object_id_node.text): + report_error("2. Invalid ObjectId UUID") + check2_ok = False + + gen_types = internal_info.findall(f"{{{XR_NS}}}GeneratedType") + if not gen_types: + report_error("2. No GeneratedType entries found") + check2_ok = False + else: + for gt in gen_types: + gt_name = gt.get("name", "") + gt_category = gt.get("category", "") + + if gt_category != "Object": + report_warn(f"2. Unexpected GeneratedType category '{gt_category}' (expected 'Object')") + + expected_prefix = f"{md_type}Object." + if gt_name and obj_name != "(unknown)" and not gt_name.startswith(expected_prefix): + report_warn(f"2. GeneratedType name '{gt_name}' does not start with '{expected_prefix}'") + + type_id = gt.find(f"{{{XR_NS}}}TypeId") + value_id = gt.find(f"{{{XR_NS}}}ValueId") + if type_id is not None and type_id.text and not GUID_PATTERN.match(type_id.text): + report_error("2. Invalid TypeId UUID in GeneratedType") + check2_ok = False + if value_id is not None and value_id.text and not GUID_PATTERN.match(value_id.text): + report_error("2. Invalid ValueId UUID in GeneratedType") + check2_ok = False + + if check2_ok: + report_ok(f"2. InternalInfo: ClassId correct, {len(gen_types)} GeneratedType") + + if stopped: + finalize() + sys.exit(1) + + # --- Check 3: Properties --- + if props_node is None: + report_error("3. Properties block missing") + else: + check3_ok = True + + if name_node is None or not name_node.text: + report_error("3. Properties: Name is missing or empty") + check3_ok = False + else: + name_val = name_node.text + if not IDENT_PATTERN.match(name_val): + report_error(f"3. Properties: Name '{name_val}' is not a valid 1C identifier") + check3_ok = False + if len(name_val) > 80: + report_warn(f"3. Properties: Name '{name_val}' exceeds 80 characters ({len(name_val)})") + + syn_node = props_node.find(f"{{{MD_NS}}}Synonym") + syn_present = False + if syn_node is not None: + syn_item = syn_node.find(f"{{{V8_NS}}}item") + if syn_item is not None: + syn_content = syn_item.find(f"{{{V8_NS}}}content") + if syn_content is not None and syn_content.text: + syn_present = True + + default_form_node = props_node.find(f"{{{MD_NS}}}DefaultForm") + default_form_val = (default_form_node.text or "").strip() if default_form_node is not None else "" + + aux_form_node = props_node.find(f"{{{MD_NS}}}AuxiliaryForm") + aux_form_val = (aux_form_node.text or "").strip() if aux_form_node is not None else "" + + main_dcs_val = "" + if md_type == "ExternalReport": + main_dcs_node = props_node.find(f"{{{MD_NS}}}MainDataCompositionSchema") + main_dcs_val = (main_dcs_node.text or "").strip() if main_dcs_node is not None else "" + + if check3_ok: + syn_info = "Synonym present" if syn_present else "no Synonym" + extras = "" + if default_form_val: + extras += ", DefaultForm set" + if main_dcs_val: + extras += ", MainDCS set" + report_ok(f'3. Properties: Name="{obj_name}", {syn_info}{extras}') + + if stopped: + finalize() + sys.exit(1) + + # --- Check 4: ChildObjects --- + child_obj_node = type_node.find(f"{{{MD_NS}}}ChildObjects") + form_names = [] + template_names = [] + + if child_obj_node is not None: + check4_ok = True + child_counts = {} + last_order = -1 + order_ok = True + + for child in child_obj_node: + if not isinstance(child.tag, str): + continue + child_tag = localname(child) + + if child_tag not in ALLOWED_CHILD_TYPES: + report_error(f"4. ChildObjects: disallowed element '{child_tag}'") + check4_ok = False + continue + + child_counts[child_tag] = child_counts.get(child_tag, 0) + 1 + + this_order = CHILD_TYPE_ORDER.get(child_tag, -1) + if this_order < last_order and order_ok: + report_warn(f"4. ChildObjects: '{child_tag}' appears after higher-order elements (expected: Attribute, TabularSection, Form, Template, Command)") + order_ok = False + last_order = this_order + + if child_tag == "Form": + form_names.append((child.text or "").strip()) + elif child_tag == "Template": + template_names.append((child.text or "").strip()) + + if check4_ok: + summary = ", ".join(f"{k}({v})" for k, v in sorted(child_counts.items(), key=lambda x: CHILD_TYPE_ORDER.get(x[0], 99))) + if summary: + report_ok(f"4. ChildObjects: {summary}") + else: + report_ok("4. ChildObjects: empty") + else: + pass # no ChildObjects — nothing to check + + if stopped: + finalize() + sys.exit(1) + + # --- Check 5: DefaultForm / MainDCS cross-references --- + check5_ok = True + + if default_form_val: + expected_prefix = f"{md_type}.{obj_name}.Form." + if default_form_val.startswith(expected_prefix): + ref_form_name = default_form_val[len(expected_prefix):] + if ref_form_name not in form_names: + report_error(f"5. DefaultForm references '{ref_form_name}', but no such Form in ChildObjects") + check5_ok = False + else: + report_warn(f"5. DefaultForm value '{default_form_val}' has unexpected prefix (expected '{expected_prefix}...')") + + if aux_form_val: + expected_prefix = f"{md_type}.{obj_name}.Form." + if aux_form_val.startswith(expected_prefix): + ref_form_name = aux_form_val[len(expected_prefix):] + if ref_form_name not in form_names: + report_error(f"5. AuxiliaryForm references '{ref_form_name}', but no such Form in ChildObjects") + check5_ok = False + + if main_dcs_val and md_type == "ExternalReport": + expected_prefix = f"ExternalReport.{obj_name}.Template." + if main_dcs_val.startswith(expected_prefix): + ref_tpl_name = main_dcs_val[len(expected_prefix):] + if ref_tpl_name not in template_names: + report_error(f"5. MainDataCompositionSchema references '{ref_tpl_name}', but no such Template in ChildObjects") + check5_ok = False + else: + report_warn(f"5. MainDataCompositionSchema value '{main_dcs_val}' has unexpected prefix") + + if check5_ok: + refs = [] + if default_form_val: + refs.append("DefaultForm") + if aux_form_val: + refs.append("AuxiliaryForm") + if main_dcs_val: + refs.append("MainDCS") + if refs: + report_ok(f"5. Cross-references: {', '.join(refs)} valid") + else: + pass # no cross-references to check + + if stopped: + finalize() + sys.exit(1) + + # --- Check 6: Attributes --- + def check_attribute(node, context): + uuid = node.get("uuid", "") + if not uuid: + report_error(f"6. {context}Attribute missing uuid") + return False + if not GUID_PATTERN.match(uuid): + report_error(f"6. {context}Attribute has invalid uuid '{uuid}'") + return False + + el_props = node.find(f"{{{MD_NS}}}Properties") + if el_props is None: + report_error(f"6. {context}Attribute (uuid={uuid}) missing Properties") + return False + + el_name = el_props.find(f"{{{MD_NS}}}Name") + if el_name is None or not el_name.text: + report_error(f"6. {context}Attribute (uuid={uuid}) missing or empty Name") + return False + + name_val = el_name.text + if not IDENT_PATTERN.match(name_val): + report_error(f"6. {context}Attribute '{name_val}' has invalid identifier") + return False + + type_el = el_props.find(f"{{{MD_NS}}}Type") + if type_el is None: + report_error(f"6. {context}Attribute '{name_val}' missing Type block") + return False + + v8_types = type_el.findall(f"{{{V8_NS}}}Type") + v8_type_sets = type_el.findall(f"{{{V8_NS}}}TypeSet") + if not v8_types and not v8_type_sets: + report_error(f"6. {context}Attribute '{name_val}' Type block has no v8:Type or v8:TypeSet") + return False + + return True + + if child_obj_node is not None: + attrs = child_obj_node.findall(f"{{{MD_NS}}}Attribute") + check6_ok = True + attr_count = 0 + + for attr in attrs: + if stopped: + break + ok = check_attribute(attr, "") + if not ok: + check6_ok = False + attr_count += 1 + + if attr_count > 0: + if check6_ok: + report_ok(f"6. Attributes: {attr_count} checked (UUID, Name, Type)") + else: + pass # no attributes + else: + pass # no ChildObjects + + if stopped: + finalize() + sys.exit(1) + + # --- Check 7: TabularSections --- + if child_obj_node is not None: + ts_sections = child_obj_node.findall(f"{{{MD_NS}}}TabularSection") + if ts_sections: + check7_ok = True + ts_count = 0 + ts_attr_total = 0 + + for ts in ts_sections: + if stopped: + break + ts_count += 1 + + ts_uuid = ts.get("uuid", "") + if not ts_uuid or not GUID_PATTERN.match(ts_uuid): + report_error(f"7. TabularSection #{ts_count}: invalid or missing uuid") + check7_ok = False + + ts_props = ts.find(f"{{{MD_NS}}}Properties") + ts_name_node = ts_props.find(f"{{{MD_NS}}}Name") if ts_props is not None else None + ts_name = ts_name_node.text if ts_name_node is not None and ts_name_node.text else "(unnamed)" + + if ts_name_node is None or not ts_name_node.text: + report_error(f"7. TabularSection #{ts_count}: missing or empty Name") + check7_ok = False + elif not IDENT_PATTERN.match(ts_name): + report_error(f"7. TabularSection '{ts_name}': invalid identifier") + check7_ok = False + + ts_int_info = ts.find(f"{{{MD_NS}}}InternalInfo") + if ts_int_info is not None: + ts_gens = ts_int_info.findall(f"{{{XR_NS}}}GeneratedType") + if len(ts_gens) < 2: + report_warn(f"7. TabularSection '{ts_name}': expected 2 GeneratedType, found {len(ts_gens)}") + + ts_child_obj = ts.find(f"{{{MD_NS}}}ChildObjects") + if ts_child_obj is not None: + ts_attrs = ts_child_obj.findall(f"{{{MD_NS}}}Attribute") + ts_attr_names = {} + for ta in ts_attrs: + ta_ok = check_attribute(ta, f"TabularSection '{ts_name}'.") + if not ta_ok: + check7_ok = False + ts_attr_total += 1 + + ta_props = ta.find(f"{{{MD_NS}}}Properties") + if ta_props is not None: + ta_name_node = ta_props.find(f"{{{MD_NS}}}Name") + if ta_name_node is not None and ta_name_node.text: + if ta_name_node.text in ts_attr_names: + report_error(f"7. Duplicate attribute '{ta_name_node.text}' in TabularSection '{ts_name}'") + check7_ok = False + else: + ts_attr_names[ta_name_node.text] = True + + if check7_ok: + report_ok(f"7. TabularSections: {ts_count} sections, {ts_attr_total} inner attributes") + else: + pass # no tabular sections + else: + pass # no ChildObjects + + if stopped: + finalize() + sys.exit(1) + + # --- Check 8: Name uniqueness --- + check8_ok = True + all_names = {} + + if child_obj_node is not None: + name_kinds = [ + ("Attribute", f"{{{MD_NS}}}Attribute"), + ("TabularSection", f"{{{MD_NS}}}TabularSection"), + ("Command", f"{{{MD_NS}}}Command"), + ] + + for kind, xpath in name_kinds: + nodes = child_obj_node.findall(xpath) + for node in nodes: + np = node.find(f"{{{MD_NS}}}Properties") + if np is not None: + nn = np.find(f"{{{MD_NS}}}Name") + if nn is not None and nn.text: + nv = nn.text + if nv in all_names: + report_error(f"8. Duplicate name '{nv}' ({kind} conflicts with {all_names[nv]})") + check8_ok = False + else: + all_names[nv] = kind + + for fn in form_names: + if fn in all_names: + report_error(f"8. Duplicate name '{fn}' (Form conflicts with {all_names[fn]})") + check8_ok = False + else: + all_names[fn] = "Form" + for tn in template_names: + if tn in all_names: + report_error(f"8. Duplicate name '{tn}' (Template conflicts with {all_names[tn]})") + check8_ok = False + else: + all_names[tn] = "Template" + + if check8_ok: + report_ok(f"8. Name uniqueness: {len(all_names)} names, all unique") + + if stopped: + finalize() + sys.exit(1) + + # --- Check 9: File existence --- + check9_ok = True + files_checked = 0 + obj_dir = os.path.join(src_dir, obj_name) + + for fn in form_names: + form_meta_xml = os.path.join(obj_dir, "Forms", f"{fn}.xml") + if not os.path.isfile(form_meta_xml): + report_error(f"9. Missing form descriptor: Forms/{fn}.xml") + check9_ok = False + else: + files_checked += 1 + + form_xml = os.path.join(obj_dir, "Forms", fn, "Ext", "Form.xml") + if not os.path.isfile(form_xml): + report_error(f"9. Missing form layout: Forms/{fn}/Ext/Form.xml") + check9_ok = False + else: + files_checked += 1 + + for tn in template_names: + tpl_meta_xml = os.path.join(obj_dir, "Templates", f"{tn}.xml") + if not os.path.isfile(tpl_meta_xml): + report_error(f"9. Missing template descriptor: Templates/{tn}.xml") + check9_ok = False + else: + files_checked += 1 + + tpl_ext_dir = os.path.join(obj_dir, "Templates", tn, "Ext") + if os.path.isdir(tpl_ext_dir): + tpl_files = [f for f in os.listdir(tpl_ext_dir) if f.startswith("Template.")] + if not tpl_files: + report_error(f"9. Missing template content: Templates/{tn}/Ext/Template.*") + check9_ok = False + else: + files_checked += 1 + else: + report_error(f"9. Missing template Ext directory: Templates/{tn}/Ext/") + check9_ok = False + + obj_module = os.path.join(obj_dir, "Ext", "ObjectModule.bsl") + if os.path.isfile(obj_module): + files_checked += 1 + + if check9_ok: + if files_checked > 0: + report_ok(f"9. File existence: {files_checked} files verified") + else: + pass # no forms/templates to check + + if stopped: + finalize() + sys.exit(1) + + # --- Check 10: Form descriptors structure --- + check10_ok = True + forms_checked = 0 + + for fn in form_names: + form_meta_xml = os.path.join(obj_dir, "Forms", f"{fn}.xml") + if not os.path.isfile(form_meta_xml): + continue + + try: + f_parser = etree.XMLParser(remove_blank_text=True) + f_tree = etree.parse(form_meta_xml, f_parser) + f_root = f_tree.getroot() + + if localname(f_root) != "MetaDataObject": + report_error(f"10. Form '{fn}': root element is '{localname(f_root)}', expected 'MetaDataObject'") + check10_ok = False + continue + + f_type_node = f_root.find(f"{{{MD_NS}}}Form") + if f_type_node is None: + report_error(f"10. Form '{fn}': missing element") + check10_ok = False + continue + + f_uuid = f_type_node.get("uuid", "") + if not f_uuid or not GUID_PATTERN.match(f_uuid): + report_error(f"10. Form '{fn}': invalid or missing uuid") + check10_ok = False + + f_props = f_type_node.find(f"{{{MD_NS}}}Properties") + if f_props is not None: + f_name = f_props.find(f"{{{MD_NS}}}Name") + if f_name is not None and f_name.text != fn: + report_error(f"10. Form '{fn}': Name in descriptor is '{f_name.text}', expected '{fn}'") + check10_ok = False + + f_type = f_props.find(f"{{{MD_NS}}}FormType") + if f_type is not None and f_type.text != "Managed": + report_warn(f"10. Form '{fn}': FormType is '{f_type.text}' (expected 'Managed')") + + forms_checked += 1 + except Exception as e: + report_error(f"10. Form '{fn}': XML parse error: {e}") + check10_ok = False + + if check10_ok: + if forms_checked > 0: + report_ok(f"10. Form descriptors: {forms_checked} checked") + else: + pass # no form descriptors to check + + # --- Final output --- + finalize() + + if errors > 0: + sys.exit(1) + sys.exit(0) + + +if __name__ == "__main__": + main() diff --git a/.codex/skills/erf-build/SKILL.md b/.codex/skills/erf-build/SKILL.md new file mode 100644 index 00000000..6515f53e --- /dev/null +++ b/.codex/skills/erf-build/SKILL.md @@ -0,0 +1,71 @@ +--- +name: erf-build +description: Собрать внешний отчёт 1С (ERF) из XML-исходников. Используй когда пользователь просит собрать, скомпилировать отчёт или получить ERF файл из исходников +argument-hint: +allowed-tools: + - Bash + - Read + - Glob + - Grep +--- + +# /erf-build — Сборка отчёта + +## Usage + +``` +/erf-build [SrcDir] [OutDir] +``` + +| Параметр | Обязательный | По умолчанию | Описание | +|------------|:------------:|--------------|--------------------------------------| +| ReportName | да | — | Имя отчёта (имя корневого XML) | +| SrcDir | нет | `src` | Каталог исходников | +| OutDir | нет | `build` | Каталог для результата | + +## Параметры подключения (опционально) + +Предпочтительно использовать конкретную базу — это надёжнее и не требует создания временной базы. + +1. Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` и разреши базу: +2. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую +3. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` +4. Если не указал — сопоставь текущую ветку Git с `databases[].branches` +5. Если ветка не совпала — используй `default` +6. Если `.v8-project.json` нет или база не найдена — не указывай параметры подключения: скрипт автоматически создаст временную базу. Для ERF со ссылочными типами (CatalogRef, DocumentRef и т.д.) генерируются заглушки метаданных. Временная база удаляется после сборки. + +Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1` +Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. + +## Команда + +Используй общий скрипт из epf-build: + +```powershell +powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/epf-build/scripts/epf-build.ps1" <параметры> +``` + +### Параметры скрипта + +| Параметр | Обязательный | Описание | +|----------|:------------:|----------| +| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) | +| `-InfoBasePath <путь>` | * | Файловая база | +| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) | +| `-InfoBaseRef <имя>` | * | Имя базы на сервере | +| `-UserName <имя>` | нет | Имя пользователя | +| `-Password <пароль>` | нет | Пароль | +| `-SourceFile <путь>` | да | Путь к корневому XML-файлу исходников | +| `-OutputFile <путь>` | да | Путь к выходному ERF-файлу | + +> `*` — опционально. Если не указано — автоматически создаётся временная база со заглушками метаданных + +## Примеры + +```powershell +# Сборка отчёта (файловая база) +powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/epf-build/scripts/epf-build.ps1" -InfoBasePath "C:\Bases\MyDB" -SourceFile "src/МойОтчёт.xml" -OutputFile "build/МойОтчёт.erf" + +# Серверная база +powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/epf-build/scripts/epf-build.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -SourceFile "src/МойОтчёт.xml" -OutputFile "build/МойОтчёт.erf" +``` diff --git a/.codex/skills/erf-dump/SKILL.md b/.codex/skills/erf-dump/SKILL.md new file mode 100644 index 00000000..d7042df3 --- /dev/null +++ b/.codex/skills/erf-dump/SKILL.md @@ -0,0 +1,71 @@ +--- +name: erf-dump +description: Разобрать ERF-файл отчёта 1С в XML-исходники. Используй когда пользователь просит разобрать, декомпилировать отчёт, получить исходники из ERF файла +argument-hint: +allowed-tools: + - Bash + - Read + - Glob + - Grep +--- + +# /erf-dump — Разборка отчёта + +## Usage + +``` +/erf-dump [OutDir] +``` + +| Параметр | Обязательный | По умолчанию | Описание | +|----------|:------------:|--------------|-------------------------------------| +| ErfFile | да | — | Путь к ERF-файлу | +| OutDir | нет | `src` | Каталог для выгрузки исходников | + +## Параметры подключения (обязательно) + +Для разборки EPF/ERF требуется информационная база с конфигурацией. Без базы ссылочные типы безвозвратно теряются. + +1. Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` и разреши базу: +2. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую +3. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` +4. Если не указал — сопоставь текущую ветку Git с `databases[].branches` +5. Если ветка не совпала — используй `default` +6. Если `.v8-project.json` нет или база не найдена — **сообщи пользователю об ошибке**. Для dump база обязательна: в пустой базе ссылочные типы (CatalogRef, DocumentRef и т.д.) безвозвратно сбрасываются в строки. Предложи указать базу или зарегистрировать через `/db-list add`. + +Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1` +Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. + +## Команда + +Используй общий скрипт из epf-dump: + +```powershell +powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/epf-dump/scripts/epf-dump.ps1" <параметры> +``` + +### Параметры скрипта + +| Параметр | Обязательный | Описание | +|----------|:------------:|----------| +| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) | +| `-InfoBasePath <путь>` | * | Файловая база | +| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) | +| `-InfoBaseRef <имя>` | * | Имя базы на сервере | +| `-UserName <имя>` | нет | Имя пользователя | +| `-Password <пароль>` | нет | Пароль | +| `-InputFile <путь>` | да | Путь к ERF-файлу | +| `-OutputDir <путь>` | да | Каталог для выгрузки исходников | +| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` | + +> `*` — обязательно хотя бы одно подключение. Без базы скрипт завершится с ошибкой (dump в пустой базе безвозвратно теряет ссылочные типы) + +## Примеры + +```powershell +# Разборка отчёта (файловая база) +powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/epf-dump/scripts/epf-dump.ps1" -InfoBasePath "C:\Bases\MyDB" -InputFile "build/МойОтчёт.erf" -OutputDir "src" + +# Серверная база +powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/epf-dump/scripts/epf-dump.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -InputFile "build/МойОтчёт.erf" -OutputDir "src" +``` diff --git a/.codex/skills/erf-init/SKILL.md b/.codex/skills/erf-init/SKILL.md new file mode 100644 index 00000000..25b83d4b --- /dev/null +++ b/.codex/skills/erf-init/SKILL.md @@ -0,0 +1,42 @@ +--- +name: erf-init +description: Создать пустой внешний отчёт 1С (scaffold XML-исходников). Используй когда нужно создать новый внешний отчёт с нуля +argument-hint: [Synonym] [--with-skd] +allowed-tools: + - Bash + - Read + - Write + - Edit + - Glob + - Grep +--- + +# /erf-init — Создание нового отчёта + +Генерирует минимальный набор XML-исходников для внешнего отчёта 1С: корневой файл метаданных и каталог отчёта. + +## Usage + +``` +/erf-init [Synonym] [SrcDir] [--with-skd] +``` + +| Параметр | Обязательный | По умолчанию | Описание | +|-----------|:------------:|--------------|---------------------------------------| +| Name | да | — | Имя отчёта (латиница/кириллица) | +| Synonym | нет | = Name | Синоним (отображаемое имя) | +| SrcDir | нет | `src` | Каталог исходников относительно CWD | +| --WithSKD | нет | — | Создать пустую СКД и привязать к MainDataCompositionSchema | + +## Команда + +```powershell +powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/erf-init/scripts/init.ps1" -Name "" [-Synonym ""] [-SrcDir ""] [-WithSKD] +``` + +## Дальнейшие шаги + +- Добавить форму: `/form-add` +- Добавить макет: `/template-add` +- Добавить справку: `/help-add` +- Собрать ERF: `/erf-build` diff --git a/.codex/skills/erf-init/scripts/init.ps1 b/.codex/skills/erf-init/scripts/init.ps1 new file mode 100644 index 00000000..9cd22885 --- /dev/null +++ b/.codex/skills/erf-init/scripts/init.ps1 @@ -0,0 +1,180 @@ +# erf-init v1.1 — Init 1C external report scaffold +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( + [Parameter(Mandatory)] + [string]$Name, + + [string]$Synonym = $Name, + + [string]$SrcDir = "src", + + [switch]$WithSKD +) + +$ErrorActionPreference = "Stop" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 +[Console]::InputEncoding = [System.Text.Encoding]::UTF8 + +$uuid1 = [guid]::NewGuid().ToString() +$uuid2 = [guid]::NewGuid().ToString() +$uuid3 = [guid]::NewGuid().ToString() +$uuid4 = [guid]::NewGuid().ToString() + +# --- Формируем Properties --- + +$mainDCSValue = "" +$childObjectsContent = "" + +if ($WithSKD) { + $mainDCSValue = "ExternalReport.$Name.Template.ОсновнаяСхемаКомпоновкиДанных" + $childObjectsContent = @" + + + +"@ +} + +$mainDCSElement = if ($mainDCSValue) { + "$mainDCSValue" +} else { + "" +} + +$childObjectsXml = if ($childObjectsContent) { + "$childObjectsContent" +} else { + "" +} + +$xml = @" + + + + + + e41aff26-25cf-4bb6-b6c1-3f478a75f374 + $uuid2 + + + $uuid3 + $uuid4 + + + + $Name + + + ru + $Synonym + + + + + + $mainDCSElement + + + + + + + $childObjectsXml + + +"@ + +$rootFile = Join-Path $SrcDir "$Name.xml" +$reportDir = Join-Path $SrcDir $Name + +if (Test-Path $rootFile) { + Write-Error "Файл уже существует: $rootFile" + exit 1 +} + +if (-not (Test-Path $SrcDir)) { + New-Item -ItemType Directory -Path $SrcDir -Force | Out-Null +} +$extDir = Join-Path $reportDir "Ext" +New-Item -ItemType Directory -Path $extDir -Force | Out-Null + +$enc = New-Object System.Text.UTF8Encoding($true) +[System.IO.File]::WriteAllText((Resolve-Path $SrcDir | Join-Path -ChildPath "$Name.xml"), $xml, $enc) + +# --- Модуль объекта --- + +$moduleBsl = @" +#Область ОписаниеПеременных + +#КонецОбласти + +#Область ПрограммныйИнтерфейс + +#КонецОбласти + +#Область СлужебныеПроцедурыИФункции + +#КонецОбласти +"@ + +$modulePath = Join-Path $extDir "ObjectModule.bsl" +[System.IO.File]::WriteAllText($modulePath, $moduleBsl, $enc) + +Write-Host "[OK] Создан отчёт: $rootFile" +Write-Host " Каталог: $reportDir" +Write-Host " Модуль: $modulePath" + +# --- СКД-макет (если --WithSKD) --- + +if ($WithSKD) { + $templatesDir = Join-Path $reportDir "Templates" + $skdName = "ОсновнаяСхемаКомпоновкиДанных" + $skdMetaPath = Join-Path $templatesDir "$skdName.xml" + $skdExtDir = Join-Path (Join-Path $templatesDir $skdName) "Ext" + New-Item -ItemType Directory -Path $skdExtDir -Force | Out-Null + + $skdUuid = [guid]::NewGuid().ToString() + + $skdMetaXml = @" + + + + +"@ + + [System.IO.File]::WriteAllText($skdMetaPath, $skdMetaXml, $enc) + + $skdContent = @" + + + + ИсточникДанных1 + Local + + +"@ + + $skdFilePath = Join-Path $skdExtDir "Template.xml" + [System.IO.File]::WriteAllText($skdFilePath, $skdContent, $enc) + + Write-Host " СКД: $skdMetaPath" + Write-Host " Тело: $skdFilePath" +} diff --git a/.codex/skills/erf-init/scripts/init.py b/.codex/skills/erf-init/scripts/init.py new file mode 100644 index 00000000..9941c74e --- /dev/null +++ b/.codex/skills/erf-init/scripts/init.py @@ -0,0 +1,167 @@ +#!/usr/bin/env python3 +# erf-init v1.1 — Init 1C external report scaffold +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +"""Generates minimal XML source files for a 1C external report.""" +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(): + sys.stdout.reconfigure(encoding="utf-8") + sys.stderr.reconfigure(encoding="utf-8") + parser = argparse.ArgumentParser(description='Init 1C external report scaffold', allow_abbrev=False) + parser.add_argument('-Name', dest='Name', required=True) + parser.add_argument('-Synonym', dest='Synonym', default=None) + parser.add_argument('-SrcDir', dest='SrcDir', default='src') + parser.add_argument('-WithSKD', dest='WithSKD', action='store_true') + args = parser.parse_args() + + name = args.Name + synonym = args.Synonym if args.Synonym else name + src_dir = args.SrcDir + + uuid1 = new_uuid() + uuid2 = new_uuid() + uuid3 = new_uuid() + uuid4 = new_uuid() + + # --- Properties --- + main_dcs_value = "" + child_objects_content = "" + + if args.WithSKD: + main_dcs_value = f"ExternalReport.{name}.Template.ОсновнаяСхемаКомпоновкиДанных" + child_objects_content = f"\n\t\t\t\n" + + main_dcs_element = f"{main_dcs_value}" if main_dcs_value else "" + child_objects_xml = f"{child_objects_content}\t\t" if child_objects_content else "" + + xml = f''' + +\t +\t\t +\t\t\t +\t\t\t\te41aff26-25cf-4bb6-b6c1-3f478a75f374 +\t\t\t\t{uuid2} +\t\t\t +\t\t\t +\t\t\t\t{uuid3} +\t\t\t\t{uuid4} +\t\t\t +\t\t +\t\t +\t\t\t{esc_xml(name)} +\t\t\t +\t\t\t\t +\t\t\t\t\tru +\t\t\t\t\t{esc_xml(synonym)} +\t\t\t\t +\t\t\t +\t\t\t +\t\t\t +\t\t\t +\t\t\t{main_dcs_element} +\t\t\t +\t\t\t +\t\t\t +\t\t\t +\t\t\t +\t\t +\t\t{child_objects_xml} +\t +''' + + root_file = os.path.join(src_dir, f"{name}.xml") + report_dir = os.path.join(src_dir, name) + + if os.path.exists(root_file): + print(f"Файл уже существует: {root_file}", file=sys.stderr) + sys.exit(1) + + os.makedirs(src_dir, exist_ok=True) + ext_dir = os.path.join(report_dir, "Ext") + os.makedirs(ext_dir, exist_ok=True) + + write_utf8_bom(os.path.join(os.path.abspath(src_dir), f"{name}.xml"), xml) + + # --- Модуль объекта --- + module_bsl = """\ +#Область ОписаниеПеременных + +#КонецОбласти + +#Область ПрограммныйИнтерфейс + +#КонецОбласти + +#Область СлужебныеПроцедурыИФункции + +#КонецОбласти""" + + module_path = os.path.join(ext_dir, "ObjectModule.bsl") + write_utf8_bom(module_path, module_bsl) + + print(f"[OK] Создан отчёт: {root_file}") + print(f" Каталог: {report_dir}") + print(f" Модуль: {module_path}") + + # --- СКД-макет --- + if args.WithSKD: + templates_dir = os.path.join(report_dir, "Templates") + skd_name = "ОсновнаяСхемаКомпоновкиДанных" + skd_meta_path = os.path.join(templates_dir, f"{skd_name}.xml") + skd_ext_dir = os.path.join(templates_dir, skd_name, "Ext") + os.makedirs(skd_ext_dir, exist_ok=True) + + skd_uuid = new_uuid() + + skd_meta_xml = f''' + +\t +''' + + write_utf8_bom(skd_meta_path, skd_meta_xml) + + skd_content = ''' + +\t +\t\tИсточникДанных1 +\t\tLocal +\t +''' + + skd_file_path = os.path.join(skd_ext_dir, "Template.xml") + write_utf8_bom(skd_file_path, skd_content) + + print(f" СКД: {skd_meta_path}") + print(f" Тело: {skd_file_path}") + +if __name__ == '__main__': + main() diff --git a/.codex/skills/erf-validate/SKILL.md b/.codex/skills/erf-validate/SKILL.md new file mode 100644 index 00000000..834a6411 --- /dev/null +++ b/.codex/skills/erf-validate/SKILL.md @@ -0,0 +1,32 @@ +--- +name: erf-validate +description: Валидация внешнего отчёта 1С (ERF). Используй после создания или модификации отчёта для проверки корректности +argument-hint: [-Detailed] [-MaxErrors 30] +allowed-tools: + - Bash + - Read + - Glob +--- + +# /erf-validate — валидация внешнего отчёта (ERF) + +Проверяет структурную корректность XML-исходников внешнего отчёта: корневую структуру, InternalInfo, свойства (включая MainDataCompositionSchema), ChildObjects, реквизиты, табличные части, уникальность имён, наличие файлов форм и макетов. + +Использует тот же скрипт, что и `/epf-validate` — автоопределение по типу элемента (ExternalReport). + +## Параметры + +| Параметр | Обяз. | Умолч. | Описание | +|------------|:-----:|---------|-------------------------------------------------| +| ObjectPath | да | — | Путь к корневому XML или каталогу отчёта | +| Detailed | нет | — | Подробный вывод (все проверки, включая успешные) | +| MaxErrors | нет | 30 | Остановиться после N ошибок | +| OutFile | нет | — | Записать результат в файл (UTF-8 BOM) | + +## Команда + +```powershell +powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/epf-validate/scripts/epf-validate.ps1" -ObjectPath "src/МойОтчёт" +powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/epf-validate/scripts/epf-validate.ps1" -ObjectPath "src/МойОтчёт/МойОтчёт.xml" +``` + diff --git a/.codex/skills/form-add/SKILL.md b/.codex/skills/form-add/SKILL.md new file mode 100644 index 00000000..dc0bac83 --- /dev/null +++ b/.codex/skills/form-add/SKILL.md @@ -0,0 +1,71 @@ +--- +name: form-add +description: Добавить пустую управляемую форму к объекту 1С. Используй когда нужно создать у объекта новую форму +argument-hint: [Purpose] [--set-default] +allowed-tools: + - Bash + - Read + - Write + - Edit + - Glob + - Grep +--- + +# /form-add — Добавление формы к объекту конфигурации + +Создаёт управляемую форму (metadata XML + Form.xml + Module.bsl) и регистрирует её в корневом XML объекта конфигурации (Document, Catalog, InformationRegister и др.). + +## Usage + +``` +/form-add [Purpose] [Synonym] [--set-default] +``` + +| Параметр | Обязательный | По умолчанию | Описание | +|-------------|:------------:|--------------|----------------------------------------------| +| ObjectPath | да | — | Путь к XML-файлу объекта (Documents/Док.xml) | +| FormName | да | — | Имя формы (ФормаДокумента) | +| Purpose | нет | Object | Назначение: Object, List, Choice, Record | +| Synonym | нет | = FormName | Синоним формы | +| --set-default | нет | авто | Установить как форму по умолчанию | + +## Команда + +```powershell +powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/form-add/scripts/form-add.ps1" -ObjectPath "" -FormName "" [-Purpose ""] [-Synonym ""] [-SetDefault] +``` + +## Purpose — назначение формы + +| Purpose | Допустимые типы объектов | Основной реквизит | DefaultForm-свойство | +|---------|-------------------------|-------------------|---------------------| +| Object | Document, Catalog, DataProcessor, Report, ExternalDataProcessor, ExternalReport, ChartOf*, ExchangePlan, BusinessProcess, Task | Объект (тип: *Object.Имя) | DefaultObjectForm (DefaultForm для DataProcessor/Report/ExternalDataProcessor/ExternalReport) | +| List | Все кроме DataProcessor | Список (DynamicList) | DefaultListForm | +| Choice | Document, Catalog, ChartOf*, ExchangePlan, BusinessProcess, Task | Список (DynamicList) | DefaultChoiceForm | +| Record | InformationRegister | Запись (InformationRegisterRecordManager) | DefaultRecordForm | + +## Примеры + +``` +# Форма документа +/form-add Documents/АвансовыйОтчет.xml ФормаДокумента --purpose Object + +# Форма списка каталога +/form-add Catalogs/Контрагенты.xml ФормаСписка --purpose List + +# Форма записи регистра сведений +/form-add InformationRegisters/КурсыВалют.xml ФормаЗаписи --purpose Record + +# Форма выбора с синонимом +/form-add Catalogs/Номенклатура.xml ФормаВыбора --purpose Choice --synonym "Выбор номенклатуры" + +# Установить как форму по умолчанию +/form-add Documents/Заказ.xml ФормаДокументаНовая --purpose Object --set-default +``` + +## Workflow + +1. `/form-add` — создать каркас формы +2. `/form-compile` или `/form-edit` — наполнить Form.xml элементами +3. `/form-validate` — проверить корректность +4. `/form-info` — проанализировать результат diff --git a/.codex/skills/form-add/scripts/form-add.ps1 b/.codex/skills/form-add/scripts/form-add.ps1 new file mode 100644 index 00000000..8a1a47ff --- /dev/null +++ b/.codex/skills/form-add/scripts/form-add.ps1 @@ -0,0 +1,478 @@ +# form-add v1.5 — Add managed form to 1C config object +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +param( + [Parameter(Mandatory)] + [string]$ObjectPath, + + [Parameter(Mandatory)] + [string]$FormName, + + [string]$Synonym = $FormName, + + [string]$Purpose = "Object", + + [switch]$SetDefault +) + +$ErrorActionPreference = "Stop" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 +[Console]::InputEncoding = [System.Text.Encoding]::UTF8 + +# --- Detect XML format version --- + +function Detect-FormatVersion([string]$dir) { + $d = $dir + while ($d) { + $cfgPath = Join-Path $d "Configuration.xml" + if (Test-Path $cfgPath) { + $head = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8).Substring(0, [Math]::Min(2000, (Get-Item $cfgPath).Length)) + if ($head -match ']+version="(\d+\.\d+)"') { return $Matches[1] } + } + $parent = Split-Path $d -Parent + if ($parent -eq $d) { break } + $d = $parent + } + return "2.17" +} + +# --- Фаза 1: Определение типа объекта --- + +# Resolve ObjectPath (directory → .xml) +if (-not [System.IO.Path]::IsPathRooted($ObjectPath)) { + $ObjectPath = Join-Path (Get-Location).Path $ObjectPath +} +if (Test-Path $ObjectPath -PathType Container) { + $dirName = Split-Path $ObjectPath -Leaf + $candidate = Join-Path $ObjectPath "$dirName.xml" + $sibling = Join-Path (Split-Path $ObjectPath) "$dirName.xml" + if (Test-Path $candidate) { $ObjectPath = $candidate } + elseif (Test-Path $sibling) { $ObjectPath = $sibling } +} + +if (-not (Test-Path $ObjectPath)) { + Write-Error "Файл объекта не найден: $ObjectPath" + exit 1 +} + +$objectXmlFull = Resolve-Path $ObjectPath +$script:formatVersion = Detect-FormatVersion (Split-Path $objectXmlFull.Path -Parent) + +$xmlDoc = New-Object System.Xml.XmlDocument +$xmlDoc.PreserveWhitespace = $true +$xmlDoc.Load($objectXmlFull.Path) + +$nsMgr = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable) +$nsMgr.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") +$nsMgr.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core") + +# Определяем тип объекта по корневому тегу внутри MetaDataObject +$metaDataObject = $xmlDoc.SelectSingleNode("//md:MetaDataObject", $nsMgr) +if (-not $metaDataObject) { + # Пробуем без namespace (fallback) + $metaDataObject = $xmlDoc.DocumentElement +} + +$supportedTypes = @( + "Document", "Catalog", "DataProcessor", "Report", + "ExternalDataProcessor", "ExternalReport", + "InformationRegister", "AccumulationRegister", "ChartOfAccounts", "ChartOfCharacteristicTypes", + "ExchangePlan", "BusinessProcess", "Task" +) + +$objectType = $null +$objectNode = $null +foreach ($t in $supportedTypes) { + $node = $xmlDoc.SelectSingleNode("//md:$t", $nsMgr) + if ($node) { + $objectType = $t + $objectNode = $node + break + } +} + +if (-not $objectType) { + Write-Error "Не удалось определить тип объекта. Поддерживаемые типы: $($supportedTypes -join ', ')" + exit 1 +} + +# Имя объекта из Properties/Name +$objectName = $xmlDoc.SelectSingleNode("//md:${objectType}/md:Properties/md:Name", $nsMgr).InnerText +if (-not $objectName) { + Write-Error "Не удалось определить имя объекта из Properties/Name" + exit 1 +} + +Write-Host "" +Write-Host "=== form-add ===" +Write-Host "" +Write-Host "Object: $objectType.$objectName" + +# --- Фаза 2: Валидация Purpose --- + +$Purpose = $Purpose.Substring(0,1).ToUpper() + $Purpose.Substring(1).ToLower() +# Нормализация +switch ($Purpose) { + "Object" { } + "List" { } + "Choice" { } + "Record" { } + default { + Write-Error "Недопустимое назначение: $Purpose. Допустимые: Object, List, Choice, Record" + exit 1 + } +} + +$objectLikeTypes = @("Document", "Catalog", "ChartOfAccounts", "ChartOfCharacteristicTypes", "ExchangePlan", "BusinessProcess", "Task") +$processorLikeTypes = @("DataProcessor", "Report", "ExternalDataProcessor", "ExternalReport") + +switch ($Purpose) { + "Object" { + # допустимо для всех типов + } + "List" { + if ($objectType -eq "DataProcessor") { + Write-Error "Purpose=List недопустим для DataProcessor" + exit 1 + } + } + "Choice" { + if ($objectType -in $processorLikeTypes -or $objectType -eq "InformationRegister") { + Write-Error "Purpose=Choice недопустим для $objectType" + exit 1 + } + } + "Record" { + if ($objectType -ne "InformationRegister") { + Write-Error "Purpose=Record допустим только для InformationRegister" + exit 1 + } + } +} + +# --- Фаза 3: Создание файлов --- + +$objectDir = [System.IO.Path]::ChangeExtension($objectXmlFull.Path, $null).TrimEnd('.') +$formsDir = Join-Path $objectDir "Forms" +$formMetaPath = Join-Path $formsDir "$FormName.xml" + +if (Test-Path $formMetaPath) { + Write-Error "Форма уже существует: $formMetaPath" + exit 1 +} + +$formDir = Join-Path $formsDir $FormName +$formExtDir = Join-Path $formDir "Ext" +$formModuleDir = Join-Path $formExtDir "Form" + +New-Item -ItemType Directory -Path $formModuleDir -Force | Out-Null + +$encBom = New-Object System.Text.UTF8Encoding($true) + +# --- 3a. Метаданные формы --- + +$formUuid = [guid]::NewGuid().ToString() + +# ExtendedPresentation — only for DataProcessor, Report, ExternalDataProcessor, ExternalReport forms +$extPresentationLine = "" +if ($objectType -in $processorLikeTypes) { + $extPresentationLine = "`n`t`t`t" +} + +$formMetaXml = @" + + + + + $FormName + + + ru + $Synonym + + + + Managed + false + + PlatformApplication + MobilePlatformApplication + $extPresentationLine + + + +"@ + +[System.IO.File]::WriteAllText($formMetaPath, $formMetaXml, $encBom) + +# --- 3b. Form.xml --- + +$formXmlPath = Join-Path $formExtDir "Form.xml" + +$formNsDecl = 'xmlns="http://v8.1c.ru/8.3/xcf/logform" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core" xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings" 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: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"' + +if ($Purpose -eq "List" -or $Purpose -eq "Choice") { + # Динамический список + # MainTable: тип.имя + $mainTable = "$objectType.$objectName" + + $formXml = @" + +
+ + true + + + + + + cfg:DynamicList + + true + + $mainTable + + + + +"@ +} elseif ($Purpose -eq "Record") { + # Запись регистра сведений + $mainAttrName = "Запись" + $mainAttrType = "InformationRegisterRecordManager.$objectName" + + $formXml = @" + +
+ + true + + + + + + cfg:$mainAttrType + + true + true + + + +"@ +} else { + # Object — форма объекта + $mainAttrName = "Объект" + + # Маппинг типа объекта на тип реквизита + $attrTypeMap = @{ + "Document" = "DocumentObject" + "Catalog" = "CatalogObject" + "DataProcessor" = "DataProcessorObject" + "Report" = "ReportObject" + "ExternalDataProcessor" = "ExternalDataProcessorObject" + "ExternalReport" = "ExternalReportObject" + "ChartOfAccounts" = "ChartOfAccountsObject" + "ChartOfCharacteristicTypes" = "ChartOfCharacteristicTypesObject" + "ExchangePlan" = "ExchangePlanObject" + "BusinessProcess" = "BusinessProcessObject" + "Task" = "TaskObject" + "InformationRegister" = "InformationRegisterRecordManager" + "AccumulationRegister" = "AccumulationRegisterRecordSet" + } + + $mainAttrType = "$($attrTypeMap[$objectType]).$objectName" + + # SavedData: standard for Catalog/Document/etc, but not for processor-like (DataProcessor/Report/External*) + $savedDataLine = "" + if ($objectType -notin $processorLikeTypes) { + $savedDataLine = "`n`t`t`ttrue" + } + + $formXml = @" + +
+ + true + + + + + + cfg:$mainAttrType + + true$savedDataLine + + + +"@ +} + +if (Test-Path $formXmlPath) { + Write-Host "[SKIP] Form.xml already exists: $formXmlPath — not overwriting" +} else { + [System.IO.File]::WriteAllText($formXmlPath, $formXml, $encBom) +} + +# --- 3c. Module.bsl --- + +$modulePath = Join-Path $formModuleDir "Module.bsl" + +$moduleBsl = @" +#Область ОбработчикиСобытийФормы + +#КонецОбласти + +#Область ОбработчикиСобытийЭлементовФормы + +#КонецОбласти + +#Область ОбработчикиКомандФормы + +#КонецОбласти + +#Область ОбработчикиОповещений + +#КонецОбласти + +#Область СлужебныеПроцедурыИФункции + +#КонецОбласти +"@ + +if (Test-Path $modulePath) { + Write-Host "[SKIP] Module.bsl already exists: $modulePath — not overwriting" +} else { + [System.IO.File]::WriteAllText($modulePath, $moduleBsl, $encBom) +} + +# --- Фаза 4: Регистрация в родительском объекте --- + +$childObjects = $xmlDoc.SelectSingleNode("//md:${objectType}/md:ChildObjects", $nsMgr) +if (-not $childObjects) { + Write-Error "Не найден элемент ChildObjects в $ObjectPath" + exit 1 +} + +# Добавить
$FormName
+$formElem = $xmlDoc.CreateElement("Form", "http://v8.1c.ru/8.3/MDClasses") +$formElem.InnerText = $FormName + +# Ищем первый