Compare commits

...

80 Commits

Author SHA1 Message Date
Nick Shirokov 71309d2bc2 fix(tests): make cf-edit/set-home-page case platform-loadable
Кейс ссылался на CommonForm.* и DataProcessor.Поиск.Form.ФормаПоиска,
которых не было в workDir — verify-snapshots падал на db-load-xml,
а cf-validate Check 9 (валидация HP form refs) ловил это только под
--with-validation, поэтому в обычной регрессии проблема была не видна.

Поскольку CommonForm пока не умеет создавать ни meta-compile, ни form-add
(он привязывается к существующему объекту-владельцу), заменил ссылки
на формы Catalog/DataProcessor — DSL-покрытие сохранено целиком:
template, string-ref, height, visibility, русский синтаксис типа,
короткая и полная форма roles. Добавлен preRun из 10 шагов
(meta-compile + form-add + role-compile) для синтеза всех ссылок.

Поддержка CommonForm в meta-compile/form-add — отдельная задача.

verify-snapshots: 1/1 ✓ (db-load-xml, db-update проходят).
Полная регрессия: 349/349 ✓.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 13:57:30 +03:00
Nick Shirokov e1f81a0cde chore(tests): refresh snapshots after meta-compile QuickChoice + form-compile AutoTitle defaults
- QuickChoice<true→false> для Catalog (07b2ec3 «дефолты QuickChoice по реальным
  конфигам» — часть снапшотов уже обновили в dc0382c, эти были пропущены)
- автоген <Title>/<AutoTitle>/<TitleLocation>/<SavedData> в формах
  (76800fc «автоген Title из имени» и серия фич form-compile)
- column-group/ — новый снапшот для кейса из 4f9d9ae (был не закоммичен)

Все 349 unit-тестов и 6 integration-тестов зелёные. verify-snapshots
(платформенная загрузка) — 201/202; отдельный pre-existing fail
cf-edit/set-home-page (Check 9 не валидирует HP form refs) разбираем
отдельно, в дифф этой регенерации не входит.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 13:45:47 +03:00
Nick Shirokov e8cb5440d8 feat(switch): emit -ExecutionPolicy Bypass for codex target
Codex on Windows launches powershell.exe as a login-shell that loads
the user profile despite -NoProfile in our SKILL.md. With Restricted
ExecutionPolicy this spams "выполнение сценариев отключено". Add
-ExecutionPolicy Bypass for codex; keep canonical -NoProfile -File for
all other platforms.

Round-trip safe: cmd_install always copies fresh from .claude/skills/,
so switching codex→cursor strips the EP flag. cmd_switch_runtime
re-emits PS commands via normalize_ps_invocation each pass, so
in-place py↔ps in .codex/skills/ keeps the flag.

Also fix a pre-existing bug in cmd_switch_runtime: file-existence
check used repo_root() instead of project_dir, so in-place runtime
switch in a foreign project always tripped skip_runtime=True and
became a no-op. The bug was masked when project_dir == repo_root
(source-repo workflow).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 12:40:25 +03:00
Nick Shirokov c496047c6c fix(skills): force UTF-8 console encoding in 7 ps1 scripts
Codex runner on Windows launches PowerShell as login-shell and decodes
stdout/stderr without UTF-8, garbling Cyrillic output. The other 51 ps1
scripts already set `[Console]::OutputEncoding = UTF8`; bring these 7
in line and add `InputEncoding = UTF8` for symmetry.

Touched: epf-init, erf-init, form-add, form-remove, help-add,
template-add, template-remove. Versions bumped in both ps1 and py
headers to keep the pair in sync.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 12:16:44 +03:00
Nick Shirokov df9541470c chore(plugins): tighten descriptions and align marketplace entries with Codex schema
- Replace overreaching "полный цикл разработки... до записи видеоинструкций" pitch with grounded one-liner matching the GitHub repo description (XML/CLI abstractions + eyes & hands for web-client testing).
- Drop non-standard per-plugin `interface.shortDescription` from .agents/plugins/marketplace.json — MarketplaceInterface only describes `displayName`, openai/plugins keeps per-plugin entries minimal (name/source/policy/category).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 11:42:12 +03:00
Nick Shirokov 27ecfb707d revert(plugins): drop UTF-8 BOM on SKILL.md — Codex YAML parser breaks on it
BOM before `---` makes Codex's strict frontmatter parser reject every
SKILL.md ("Skipped loading 66 skill(s)"). Without BOM the skills load
and execute correctly; the only remaining issue is mojibake display of
Cyrillic in SKILL.md previews — that's a Codex rendering bug, not ours.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 21:27:08 +03:00
Nick Shirokov 35bec4a2a1 fix(plugins): prepend UTF-8 BOM to SKILL.md on codex branches
Codex on Windows opens SKILL.md without a declared encoding and
defaults to CP1252, mangling Cyrillic. Adding the BOM lets the loader
auto-detect UTF-8.

Applied only on codex/codex-py builds to leave other ports untouched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 21:19:10 +03:00
Nick Shirokov e635a5be52 fix(plugins): drop invalid authentication: OFF from Codex marketplace
Codex schema only accepts `ON_INSTALL` or `ON_USE`; the field is
optional, so omit it for an unauthenticated plugin.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:26:09 +03:00
Nick Shirokov dfb258dd3f fix(plugins): prefix descriptions with [PowerShell]/[Python] for distinct cards
Codex plugin browser truncates the description, so the runtime tag
needs to appear at the start. Apply the same prefix to Claude
marketplace.json and plugin.json for consistency.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:13:33 +03:00
Nick Shirokov 1866c1335a feat(plugins): Codex plugin support + Python plugin variant for Claude
- Add 1c-skills-py to .claude-plugin/marketplace.json (→ port-claude-code-py)
- New .agents/plugins/marketplace.json — Codex marketplace with PS + Py plugins
- Templates .github/templates/{codex,claude}-plugin.json.tmpl rendered by CI
- build-ports.yml: generate plugin manifests on port-codex/port-codex-py/port-claude-code-py; Codex version YYYY.M.D+sha7 auto-bumped per push
- README: install instructions for Codex + Py variant for Claude
- .gitignore: narrow .agents/ → .agents/skills/ so the marketplace is tracked

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:08:07 +03:00
Nick Shirokov 4813ec5cf2 docs(readme): inline "Другие платформы →" link, drop verbose intro line
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 18:41:20 +03:00
Nick Shirokov 06ad2f8f99 feat(ci): expand port matrix to all 13 platforms + bump deprecated actions
- Add 20 matrix entries: copilot, augment, cline, kilo, kiro, gemini, opencode, roo, windsurf, agents (PS+Py each)
- Total: 25 port-* branches (Claude Code PS = main, plus 13 × Py + 12 × PS)
- Bump actions/checkout@v4 → @v5, actions/setup-python@v5 → @v6 (removes Node.js 20 deprecation warnings)
- README: replace "соберите локально" placeholders with real branch links for all platforms

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 18:37:35 +03:00
Nick Shirokov c8ac191a01 feat(ci): port-branches workflow + README install matrix
- .github/workflows/build-ports.yml — auto-build orphan port-* branches on push to main (matrix: claude-code-py, cursor PS+Py, codex PS+Py)
- .github/templates/README.port.md.tmpl — minimal per-port README rendered in CI
- README — new "Версии навыков для разных платформ" section under intro (3 flagships × PS+Py), extended platform table with PowerShell/Python branch links, switch.py moved to "альтернативный способ" subsection
- README — "Work in progress" reworded to "Проект живой, активно развивается"

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 18:29:52 +03:00
Nick Shirokov c9d83b1c92 docs(readme): add plugin install instructions
Document the recommended installation path via Claude Code plugin
marketplace:

  /plugin marketplace add https://github.com/Nikolay-Shirokov/cc-1c-skills
  /plugin install 1c-skills@cc-1c-skills

Plugin install becomes the recommended option; drop-in copy and
switch.py-based installs remain documented as alternatives for users
who prefer them or who target other AI platforms.

Closes #18.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 16:40:38 +03:00
Nick Shirokov b1a7e414d0 fix(switch): runtime conversion for ${CLAUDE_SKILL_DIR} paths
After the SKILL.md refactor, paths are wrapped in double quotes and
contain ${CLAUDE_SKILL_DIR}. The legacy RX_PS/RX_PY regexes captured
the leading quote into the path group and didn't accept '$', '{', '}'
characters, breaking three places:

- classify_skill_runtime: misdetected runtime since RX_PY didn't match
  python invocations of variable paths
- check_missing_files: built file paths like '"${CLAUDE_SKILL_DIR}/...py'
  that never existed → false-positive missing → runtime switch skipped
- switch_runtime_content: failed to convert PS->Py / Py->PS for skills
  using the new path format

Fix:
- Regexes now capture optional surrounding quote separately and accept
  any non-whitespace non-quote chars in the path
- New helper expand_skill_path() resolves ${CLAUDE_SKILL_DIR} to the
  actual on-disk path for file existence checks (handles cross-skill
  references via ../<other>/ too)
- check_missing_files derives skill_name from skill_dir to drive the
  expansion

Verified via:
  python scripts/switch.py claude-code --project-dir <tmp> --runtime python
  python scripts/switch.py claude-code --project-dir <tmp> --runtime powershell
  python scripts/switch.py codex --project-dir <tmp>

All produce correct output with quotes preserved and cross-skill
references resolved.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 15:56:26 +03:00
Nick Shirokov 7736ad68a0 feat(switch): handle ${CLAUDE_SKILL_DIR} for non-Claude targets
After SKILL.md refactor, paths use ${CLAUDE_SKILL_DIR} which Claude Code
substitutes natively. Other agents (Cursor, Codex, Copilot, etc.) don't
know about this variable, so switch.py now expands it to a literal path
when the target is not claude-code:

  ${CLAUDE_SKILL_DIR}/<rest>            -> <target_prefix>/<skill_name>/<rest>
  ${CLAUDE_SKILL_DIR}/../<other>/<rest> -> <target_prefix>/<other>/<rest>

For claude-code target the variable is left intact — drop-in install via
copy still works because Claude Code resolves it identically in
project, personal, and plugin scopes.

Verified by running:
  python scripts/switch.py codex --project-dir <tmp>
  python scripts/switch.py claude-code --project-dir <tmp>

Both produce correct output with 0 leftover variable literals in
non-Claude targets and full preservation in Claude target.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 15:31:08 +03:00
Nick Shirokov 501bb58ddb feat(plugin): plugin and marketplace manifests
Add `.claude-plugin/plugin.json` and `.claude-plugin/marketplace.json` so
the repo can be installed via:

  /plugin marketplace add https://github.com/Nikolay-Shirokov/cc-1c-skills
  /plugin install 1c-skills@cc-1c-skills

Plugin name `1c-skills`, marketplace name `cc-1c-skills` (matches repo
name). Version is omitted in `plugin.json` so Claude Code uses the git
commit SHA — convenient for active development without manual bumps.

Closes part of #18.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 15:30:32 +03:00
Nick Shirokov cd3a242b12 refactor(skills): portable script paths via ${CLAUDE_SKILL_DIR}
Replace literal `.claude/skills/<owner>/...` paths in SKILL.md with the
`${CLAUDE_SKILL_DIR}` variable that Claude Code substitutes at invocation
time. Same-skill references become `${CLAUDE_SKILL_DIR}/<rest>`,
cross-skill references (erf-* → epf-*) become
`${CLAUDE_SKILL_DIR}/../<other>/<rest>`. All paths now wrapped in double
quotes to handle install locations with spaces.

This unblocks plugin-mode installation: literal `.claude/skills/...`
paths fail when the skill is loaded from `~/.claude/plugins/cache/...`
or from a personal `~/.claude/skills/` install. Drop-in mode continues
to work because Claude Code resolves the variable in all install scopes.

Verified via pilot:
- Project drop-in (cc-1c-skills repo)
- Personal `~/.claude/skills/cf-info/`
- Plugin via `/plugin marketplace add <local-path>`

Scope: 61 SKILL.md, 125 path replacements (8 cross-skill).
Scripts unchanged — they already use \$PSScriptRoot and ../<sibling>
patterns that resolve correctly under the bundled cache layout.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 15:30:06 +03:00
Nick Shirokov 4f9d9aee97 feat(form-compile): группировка колонок таблицы (ColumnGroup)
Новый DSL-ключ columnGroup со значением-ориентацией horizontal/vertical/inCell
для элемента <ColumnGroup> внутри columns таблицы. Поддерживает вложение,
showTitle/showInHeader/width, тихие синонимы ColumnGroup и ГруппаКолонок.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 11:48:35 +03:00
Nick Shirokov f3466e19fd docs(form-patterns): актуализация под новые возможности form-compile
- Архетипы «Форма обработки» и «Мастер»: кнопки действий перенесены
  с нижней горизонтальной группы на главную АКП формы (autoCmdBar)
- Конвенция «ГруппаКнопок» заменена на «ФормаКоманднаяПанель»
- Принцип компоновки №3: уточнено, что кнопки идут на АКП
- Сворачиваемые группы: исправлен пример — корректный DSL
  (group: collapsible, collapsed: true) вместо несуществующих
  ключей behavior/collapsed на vertical-группе
- Полный пример формы обработки переписан под autoCmdBar
- Из свойств мастера убран commandBarLocation: None (не нужен,
  когда мы сами наполняем АКП)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 20:10:10 +03:00
Nick Shirokov ebf92a8780 feat(form-compile): свойство collapsed для сворачиваемых групп
Добавлен ключ collapsed (для group=collapsible) → <Collapsed>true</Collapsed>:
группа создаётся уже свёрнутой. Раньше DSL умел только включать
сворачиваемое поведение, но начальное состояние задать было нельзя.

Также уточнено описание united: оно про выравнивание левого края полей
ввода (сквозное/локальное), а не про объединение рамок группы.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 20:07:18 +03:00
Nick Shirokov 3d80191ca7 feat(form-compile): поддержка maxWidth/maxHeight для input и label
Добавлены численные maxWidth/maxHeight (XML <MaxWidth>/<MaxHeight>) —
типичный приём для ограничения растяжения поля при autoMaxWidth: false.
До этого DSL знал только булев autoMaxWidth, и ограничить ширину
числом было невозможно.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 19:58:36 +03:00
Nick Shirokov 6c60398406 feat(form-compile): авто-вид кнопки по контексту АКП
Кнопки внутри cmdBar/autoCmdBar/popup автоматически получают
CommandBarButton (или CommandBarHyperlink при type="hyperlink") —
указывать вид вручную не нужно. Резолвер прощающий: принимает и
короткие DSL-формы, и XML-имена в любом контексте.

Пример «Диалог загрузки файла» в SKILL.md и тест-кейс file-dialog
переведены на нативный паттерн с autoCmdBar вместо отдельной
горизонтальной группы кнопок внизу формы.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 19:41:43 +03:00
Nick Shirokov 60de083a05 docs(1c-form-spec): RadioButtonField перенесён в статистику (~8.5% форм БП)
Замер по acc_8.3.24: 658 из 7723 форм содержат RadioButtonField
(всего 1389 элементов). Раньше ошибочно числился среди не встреченных.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 18:48:29 +03:00
Nick Shirokov 5690c82ab8 docs(form-dsl-spec): radio (RadioButtonField) в спецификации DSL
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 18:45:00 +03:00
Nick Shirokov dc0382cc06 chore(tests): обновлены снапшоты после meta-compile QuickChoice defaults
Снапшоты ссылочных реквизитов теперь содержат QuickChoice=false
в соответствии с дефолтами по реальным конфигам (см. 07b2ec3).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 18:43:33 +03:00
Nick Shirokov b1e29253d5 feat(form-compile): RadioButtonField и словарь синонимов типов элементов
Поле переключателя с RadioButtonType (Auto/RadioButtons/Tumbler) и
ChoiceList (массив value+presentation). Толерантно к написанию модели:
русские имена тегов (ПолеПереключателя, RadioButtonField),
ВидПереключателя по-русски (Авто/Переключатель/Тумблер),
Перечисление.X.Y без EnumValue, синоним title для presentation,
автогенерация презентации из имени значения.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 18:43:26 +03:00
Nick Shirokov 07b2ec36af feat(meta-compile): дефолты QuickChoice по реальным конфигам
Catalog/CCT/CoA/CoCT/ExchangePlan: дефолт false (раньше true). Enum: дефолт true (теперь параметризовано). Цифры из acc_8.3.27 + erp_8.3.24 — у Catalog ~95% объектов QuickChoice=false, у Enum ~99% true.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 17:34:16 +03:00
Nick Shirokov b4514dc350 feat(form-compile): AutoMaxWidth=false по умолчанию для multiLine InputField
В реальных формах ERP/БП у ~60% многострочных полей ввода явно стоит
АвтоМаксимальнаяШирина=Ложь. Теперь form-compile проставляет это
автоматически при multiLine: true, если пользователь не задал
autoMaxWidth явно.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 19:05:50 +03:00
Nick Shirokov 8ad5d80f7f feat(form-compile): автоподстановка RowPictureDataPath для DynamicList с MainTable
Document/InformationRegister/AccumulationRegister List-генераторы теперь
прописывают `Список.DefaultPicture` (как делает ERP/БП в 594/600 форм
списка документов). Плюс fallback в эвристике DynamicList-таблицы:
если у главного реквизита есть `settings.mainTable`, поле
подставляется автоматически и для ручного DSL.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 18:39:29 +03:00
Nick Shirokov fc19df604b chore(tests): --help в runner и verify-snapshots, синхронизация README
Чтобы --help/-h/? не запускали полный прогон тестов.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 18:17:24 +03:00
Nick Shirokov 76800fc92b feat(form-compile): автоген Title из имени для узлов без источника синонима
Реквизиты формы, команды, страницы, попапы и декорации теперь получают Title,
сгенерированный из CamelCase-имени, если в DSL он не задан явно. Поля с path
и кнопки с command по-прежнему опускают Title — синоним подхватится платформой.
Главные реквизиты (Объект/Список/Запись с main=true) исключены.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 18:17:19 +03:00
Nick Shirokov 36cd63d8bb fix(form-compile): AutoTitle=false по умолчанию при заданном Title формы
Когда у формы задан Title (через defn.title или properties.title), эмитим
AutoTitle=false если пользователь явно его не указал. Иначе платформа
добавляет суффикс синонима и получается двойной заголовок (Номенклатура:
Номенклатура). В выгрузках ERP так делает ~95% форм с form-level Title.

Снапшоты form-compile/form-compile-from-object обновлены под новое поведение.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 17:22:48 +03:00
Nick Shirokov e216db5734 fix(form-compile): default TitleLocation=Right для CheckBoxField
Платформенный default TitleLocation для CheckBoxField — Left, что почти
никогда не соответствует UX-ожиданиям. В acc 8.3.27 для CheckBoxField:
Right (явно): 811, без тега (=Left): 406, None: 140, Left: 14, Top: 3 —
доминирующий паттерн «заголовок справа от флажка».

Эмитим <TitleLocation>Right</TitleLocation> по умолчанию для check.
Переопределяется через titleLocation: 'Left' / 'None' / 'Top' / 'Bottom'.

v1.11. Обновил 5 snapshot'ов.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 16:59:31 +03:00
Nick Shirokov a59be4b914 fix(form-compile): SavedData=true для object/RecordManager главного реквизита
Без <SavedData>true</SavedData> платформа не маркирует форму как modified
при изменении главного реквизита — confirmation dialog при Esc не появляется,
canonical flow «изменил → Esc → Да» сломан.

Правило выведено из реальной выгрузки acc 8.3.27: SavedData ставится только
для редактируемых форм с типом *Object.X (Catalog/Document/ChartOf…/
ExchangePlan/BusinessProcess/Task) или *RecordManager.X. DynamicList/Report/
DataProcessor/ConstantsSet/RecordSet — не трогаем, отдаём решение DSL через
явный savedData: true.

Поднял версию до v1.10. Обновил 5 snapshot'ов (формы элементов/документа).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 16:51:43 +03:00
Nick Shirokov 4b8d3b67dc fix(tests): platform-verifiable form-info DSL + external setup в verify-snapshots
- form-info/{rich-form,simple-form}: тип реквизита Объект исправлен
  с ExternalDataProcessorObject на DataProcessorObject — preRun создаёт
  обычную обработку, и платформа отвергала несоответствие XDTO-исключением.
- verify-snapshots.mjs: поддержка setup="external:<path>" — копирует
  внешний дамп (выгрузку ERP/БП) в workDir, пропускает cf-init и
  авто-регистрацию объектов через cf-edit. Платформенная загрузка
  целой вендорской конфы скипается — кейсы real-acc-form-* проверяют
  скрипт против реального XML, а не пригодность всей БП к чистой базе.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 14:31:18 +03:00
Nick Shirokov 6650d2b516 feat(form-info): показывать главную AutoCommandBar формы с учётом CommandBarLocation
Раньше AutoCommandBar (id=-1) полностью скрывалась как companion-элемент.
Из-за этого модель не видела главную панель и часто добавляла избыточную
дополнительную cmdBar снизу формы по образцу старых сгенерённых форм.

Теперь:
- AutoCommandBar формы выводится отдельным разделом с флагами
  (autofill/no-autofill, align=...) и списком кастомных кнопок.
- Позиция секции зависит от свойства формы CommandBarLocation:
  Auto/Top — над деревом элементов (нативное поведение платформы),
  Bottom — под деревом, None — секция скрыта.
- Если панель пустая с дефолтным autofill — выводится одной строкой.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 21:04:26 +03:00
Nick Shirokov 6b63177687 feat(form-compile): не эмитить HorizontalAlign по умолчанию + чистка SKILL.md
- HorizontalAlign не эмитится, если не задан явно через autoCmdBar.horizontalAlign.
  Раньше был хардкод Right; платформенный дефолт — Left, эталонные формы
  типовых (Бригады/ФормаСписка) тег вообще не содержат.
- horizontalAlign принимает Left/Center/Right.
- SKILL.md: убран раздел «Эвристики компилятора» (внутренняя кухня скрипта),
  сжато описание autoCmdBar и main — модель пишет DSL явно, помощь
  компилятора видна в [INFO]/[WARN]-логе.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 20:54:57 +03:00
Nick Shirokov 13174b63b1 feat(form-compile): нативная AutoCommandBar формы + autoCmdBar DSL
- Эвристика главной АКП: без cmdBar/autoCmdBar остаётся Autofill=true
  (как в Конфигураторе), с cmdBar — Autofill=false (обратная совместимость).
- Новый элемент autoCmdBar для наполнения главной АКП кастомными кнопками.
- Тихие синонимы commandBar↔cmdBar, autoCommandBar↔autoCmdBar.
- Инференс main-реквизита по типу (*Object.*, *RecordSet.*, DynamicList,
  ConstantsSet) — единственный кандидат проставляется молча с [INFO].
- Эвристика DynamicList → таблица: tableAutofill=false +
  commandBarLocation=None для привязанной таблицы (соответствие ERP).
- Косметика: <Autofill>true</Autofill> не эмитится явно.

Snapshot'ы form-* также обновлены до актуального состояния cf-init
(Ext/ClientApplicationInterface.xml).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 20:46:23 +03:00
Nick Shirokov 433a2955b5 fix(form-compile): лояльно стрипать cfg: префикс главного реквизита
Раньше тип `cfg:CatalogObject.X` мимо regex попадал в fallback и получал
второй `cfg:` поверх → `cfg:cfg:...`, db-load-xml падал. Теперь
Resolve-TypeStr срезает ведущий `cfg:` сразу, обе формы записи валидны.
Заодно сообщение об ошибке FormDataStructure согласовано с примером —
без префикса.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 19:32:53 +03:00
Nick Shirokov 42b96bbd21 feat(cf-*): set-home-page + drill-down -Section home-page + form-ref валидация
- cf-edit: новая операция set-home-page перезаписывает Ext/HomePageWorkArea.xml.
  DSL принимает template (OneColumn/TwoColumnsEqualWidth/TwoColumnsVariableWidth),
  left/right с записями форм (строка или объект form/height/visibility/roles).
  Тихая нормализация ссылок: русские типы, 3-сегмент → авто-Form, файловые пути
- cf-info: краткая HP-сводка (template + счётчики) в overview/full, детальный
  вид через -Section home-page (alias -Name) с раскладкой и переопределениями ролей
- cf-validate: Check 9 — валидация ссылок на формы из HomePageWorkArea и
  Default*Form свойств; битая ссылка → error
- reference.md: убран реализационный шум (canonical sort, авто-нормализация форм,
  panelDef detail, секция авто-валидации); путь src/ в примерах вместо репо-специфичных

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 19:13:18 +03:00
Nick Shirokov bd3b40852a feat(cf-edit): set-panels принимает русские имена панелей silently
cf-info отображает «Открытых», «Разделов», «Избранного», «История», «Функций»
(совпадает с подписями Конфигуратора). Если модель копирует эти названия в
set-panels value — теперь они тихо мапятся в каноничные английские алиасы.
Документация и сообщения об ошибках упоминают только английские формы.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 17:05:38 +03:00
Nick Shirokov 336dade274 feat(cf-edit): операция set-panels для раскладки панелей
JSON-DSL уровня имён: алиасы sections/open/favorites/history/functions
для платформенных uuid, объект {group:[...]} для стека (даёт <group>-
вложенность как у Конфигуратора), несколько записей в одной стороне =
соседние теги (рядом). Файл Ext/ClientApplicationInterface.xml
перезаписывается полностью; panelDef для всех 5 панелей пишется всегда.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 17:03:57 +03:00
Nick Shirokov 99e9c1e0b8 feat(cf-info): показывать раскладку панелей в overview/full
Читает Ext/ClientApplicationInterface.xml (если есть) и выводит секцию
«Раскладка панелей» с маппингом UUID → имя для 5 платформенных панелей.
Стек панелей внутри одной стороны отображается как «Стек(a, b)»,
несколько отдельных тегов стороны (рядом) — через « | ».

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 16:57:53 +03:00
Nick Shirokov 3c3ed2ff46 feat(cf-init): генерить Ext/ClientApplicationInterface.xml с ERP-дефолтом
Без этого файла веб-клиент 1С рендерит секции icon-only (без подписей),
а web-test их не видит. Дефолтная раскладка как в типовых ERP/БП ≥ 8.3.24:
панель открытых сверху, панель разделов слева; функций/избранного/истории
объявлены через panelDef но не размещены.

Расширил docs/1c-configuration-spec.md § 4.2 моделью раскладки и таблицей
UUID 5 платформенных панелей. Обновил снапшоты cf-init под новый файл.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 16:43:39 +03:00
Nick Shirokov d75b4d96ca fix(verify-snapshots): структурные зависимости с preRun и ChartOfAccounts
getStructuralDeps теперь сканирует все inputs (caseData.input + preRun.input),
а не только верхний — без этого регистры, созданные через preRun, оставались
без документа-регистратора и платформа отвергала конфиг с
«Ни один из документов не является регистратором».

Добавлен случай ChartOfAccounts: при maxExtDimensionCount>0 (или непустых
extDimensionAccountingFlags) и без явной ссылки extDimensionTypes
автоматически создаётся стаб ChartOfCharacteristicTypes и линкуется через
meta-edit modify-property уже после preRun. Для этого getStructuralDeps
возвращает теперь { deps, hostEdits }, а в основной поток добавлен Step 3.5
(host-level structural edits).

InformationRegister с writeMode=Subordinate/RecorderSubordinate тоже
получает документ-регистратор.

Полный прогон verify-snapshots: 193/193.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 20:12:12 +03:00
Nick Shirokov 704fbc9f28 feat(verify-snapshots): платформенная верификация mxl-compile
Готовый Template.xml оборачивается в исходники EPF: epf-init создаёт
скелет, template-add регистрирует SpreadsheetDocument-макет, MXL копируется
поверх дефолтного, далее epf-build реально проверяет, что платформа
принимает разметку. mxl-compile убран из STANDALONE_SKILLS.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 16:59:06 +03:00
Nick Shirokov cd74afa5e0 feat(verify-snapshots): платформенная верификация template-add и help-add
После main-скрипта определяется тип артефакта в workDir: Configuration.xml
→ обычная загрузка конфигурации, иначе по корню *.xml выбирается ветка
epf-build (.epf для ExternalDataProcessor, .erf для ExternalReport). Для
кейсов с cf-init теперь configDir переустанавливается на workDir, так что
конфиг действительно грузится в БД, а не пропускается.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 16:42:00 +03:00
Nick Shirokov 370873511d feat(verify-snapshots): платформенная верификация epf-init и erf-init
EPF_SKILLS превращён из «skip» в реальную сборку через epf-build —
строится .epf или .erf по расширению из map. Имя источника берётся
из caseData.params.name. erf-init добавлен в DEFAULT_SKILLS.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 15:25:20 +03:00
Nick Shirokov 9ced410d8f feat(verify-snapshots): платформенная верификация skd-edit
skd-edit добавлен в SKD_PLATFORM_VERIFY — результат заворачивается в ERF и
собирается через epf-build, как уже сделано для skd-compile. Резолвер пути
шаблона теперь учитывает params.templatePath (фолбэк: outputPath, Template.xml).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 15:09:38 +03:00
Nick Shirokov 50ba38a9f6 docs(skd-compile): корректные имена ресурсов оборотов в примере @autoDates
Виртуальная таблица .Обороты(...) возвращает ресурсы регистра с
суффиксом «Оборот» — Продажи.Количество в запросе к Обороты не
существует, нужно Продажи.КоличествоОборот. Чтобы поле в наборе
осталось «Количество» (и совпадало с totalFields/fields), добавлен
алиас КАК.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 19:08:32 +03:00
Nick Shirokov d4b105bbe4 docs(skd-compile): починить пример «С ресурсами, параметрами и @autoDates»
В прежнем примере @autoDates не приносил пользы (запрос к физической
таблице регистра, а не к виртуальной Обороты), а Организация
использовалась в filter и structure, но не выбиралась в запросе и
не была описана в fields — так схема не собралась бы или собралась
бы с пустыми колонками.

Чиню:
- запрос → РегистрНакопления.Продажи.Обороты(&НачалоПериода, &КонецПериода)
- добавлено поле Организация: СправочникСсылка.Организации @dimension
- selection — явный список без "Auto" (все поля и так перечислены)
- dataParameters: "auto" вместо ручного перечисления

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 19:00:33 +03:00
Nick Shirokov 204a262746 feat(verify-snapshots): реальная платформенная проверка skd-compile
До сих пор для skd-compile (как и других STANDALONE_SKILLS)
verify-snapshots просто запускал скрипт и помечал PASS — без
платформенной нагрузки. Опасный пробел: можно было закоммитить
snapshot, который 1С Designer не примет.

Теперь для skd-compile snapshot оборачивается во внешний отчёт
(erf-init --WithSKD), Template.xml подменяется на сгенерированный
кейсом, и запускается erf-build. Платформа парсит схему — если
принимает, кейс PASS; если отклоняет, в errors попадает её stderr.
Ссылочные типы (CatalogRef.X и т.п.) не требуют реальной базы:
epf-build сам поднимает временную stub-конфигурацию.

Если v8 недоступен — мягкий skip с пометкой "no v8 context".

Замер: 21 кейс x ~5s avg = ~110s на полный verify-snapshots
--skill skd-compile. Все 21 текущих кейса проходят — значит каждый
snapshot гарантированно платформо-валиден.

Аналогичная обёртка для mxl-compile / role-compile — отдельной
задачей по образцу.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 18:56:13 +03:00
Nick Shirokov 210bde7b2e docs(skd-compile): убрать список синонимов типов из раздела «Поля»
Перечень русских/альтернативных синонимов (`число`, `строка`,
`СправочникСсылка.X`, `int`, `bool` и т.п.) — справочный шум для
модели-пользователя: она по умолчанию пишет канонические английские
имена, а парсер тихо принимает синонимы через Resolve-TypeStr без
необходимости их декларировать в SKILL.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 18:38:34 +03:00
Nick Shirokov a932841418 fix(skd-info): убрать лишний пробел в 'Templates: N defined ( bindings)'
При схеме без field- и group-привязок строка вывода Templates выглядела
как 'Templates: 4 defined ( bindings)' — пустой блок с одиноким пробелом
перед 'bindings)'. Теперь, когда привязок нет, скобки опускаются:
'Templates: 4 defined'.

Версия v1.3.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 18:32:47 +03:00
Nick Shirokov 576f6dda8a feat(skd-compile): многоязычный title/presentation (объектная форма)
Везде, где DSL принимает title/presentation, теперь поддерживается
объектная форма с языками: "title": { "ru": "...", "en": "..." }.
Строка по-прежнему работает как ru-only.

Покрыто: field title, calculatedField title, parameter title/presentation,
settingsVariant title/presentation (root и в structure-items),
availableValue title/presentation, userSettingPresentation в filter/dataParameter,
mltext-значения в conditionalAppearance.appearance (ключи Текст/Заголовок/Формат).

Реализация:
- Хелпер emit_mltext / Emit-MLText расширен — принимает string|dict и
  итерирует по языкам.
- 8 inline-блоков LocalStringType в каждом скрипте заменены на вызовы
  хелпера (унификация — побочный эффект, бенефит на будущее).
- На входе сняты str()/"$()" коэрции для title/presentation, чтобы dict
  доходил до хелпера живым.

- SKILL.md: одна строка про объектную форму title.
- tests: новый snapshot-кейс multi-lang-title (5 узлов с ru+en).
- Версия v1.21.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 18:30:37 +03:00
Nick Shirokov 1a5b788305 feat(skd-compile): составной тип поля valueType (массив типов)
В объектной форме поля DataSet ключ "type" теперь принимает массив:
"type": ["CatalogRef.A", "CatalogRef.B"] — генерирует несколько <v8:Type>
внутри одного <valueType>. Типичный паттерн в ERP для полей-расшифровок
с составным ссылочным типом.

Shorthand остаётся одно-типовым (без перегрузки). Квалификаторы
((N)/(D,F)) применяются к каждому элементу массива независимо.

- skd-compile.ps1/py: Emit-ValueType/emit_value_type диспатчат на
  Emit-SingleValueType при строке или итерируют при массиве; field-парсер
  сохраняет массив, не приводя к строке. Версия v1.20.
- SKILL.md: один абзац после описания типов.
- tests: новый snapshot-кейс field-multi-type на 3 ссылочных типа.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 18:04:49 +03:00
Nick Shirokov b39da27d20 docs(skd-compile): описать presentationExpression и appearance на поле
В DSL skd-compile уже поддерживались ключи presentationExpression и
appearance в объектной форме поля DataSet, но в SKILL.md они не были
задокументированы — фичи существовали де-факто, но обнаружить их можно
было только чтением скрипта.

Заодно зафиксирован детерминизм порядка ключей appearance: PS5.1
hashtable не сохраняет порядок вставки, из-за чего PS- и PY-рантаймы
давали разный XML на одном входе. Заменено на [ordered]@{}.

- SKILL.md: новый блок «Дополнительные ключи объектной формы» в разделе «Поля»
- skd-compile.ps1/py: appearance = [ordered]@{} вместо @{}, версия v1.19
- tests: новый snapshot-кейс field-appearance-and-presentation,
  проходит на обоих рантаймах

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 17:57:04 +03:00
Nick Shirokov 1f23afe6f9 feat(skd-info): -Mode full перечисляет внешние наборы
Когда DataSetQuery нет, в секции query вместо безликого
"(no query datasets)" теперь печатается список objectName из
DataSetObject: "(no query datasets; external datasets: <names>)".
Не нужно скроллить вверх к Overview, чтобы увидеть источник схемы.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 16:47:01 +03:00
Nick Shirokov eda7279de0 fix(skd-info): -Mode full на схеме без DataSetQuery
Show-Query/show_query при отсутствии DataSetQuery делал exit 1, что
обрывало full режим после Show-Overview — секции fields/resources/
params/variant пользователь не видел. Теперь в full проверяем наличие
Query-набора и при отсутствии печатаем "(no query datasets)" и
продолжаем. Прямой -Mode query сохраняет прежнее поведение.

Воспроизводилось на схемах-приёмниках с одним DataSetObject
(например, ЖурналОшибок в ERP).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 16:38:57 +03:00
Nick Shirokov b3c2602439 docs(skd-compile): добавить пример DataSetObject
Внешний набор данных (objectName) был упомянут одной фразой в
type-dispatch summary, без примера. Добавлен компактный JSON-пример
+ короткое объяснение как объект подключается к
ПроцессорКомпоновкиДанных.Инициализировать.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 16:32:54 +03:00
Nick Shirokov 853313faed feat(skills): тихий -Path алиас для input-параметров
Добавлен Alias('Path') / "-Path" к основному файловому параметру
в *-info, *-validate, *-edit, *-decompile (24 навыка × PS+PY).
Не документируется — fallback на случай если модель напишет -Path
вместо -TemplatePath/-FormPath/-ObjectPath/-SubsystemPath/-RightsPath/
-ConfigPath/-ExtensionPath/-CIPath. Поведение строго аддитивное.

Регресс: 336/336 PS, 336/336 PY.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 16:17:30 +03:00
Nick Shirokov b7fbede819 fix(tests): починить pre-existing фейлы integration и skd-* snapshots
- build-config/build-epf: заменить runtime-тип FormDataStructure на корректный *Object.XXX
- platform-cfe/config/epf: form-compile принимает -OutputPath (путь до Form.xml), не -FormPath
- skd-edit/info/validate: перегенерированы snapshots после feat(skd-compile) denyIncompleteValues=true (3729b63)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 15:48:23 +03:00
Nick Shirokov bdc38caffa refactor(form-add): объединить с epf-add-form, удалить специфичный навык
form-add теперь покрывает и объекты конфигурации, и standalone EPF/ERF
source tree (тип определяется из корневого XML, маппинг типов уже был).

Изменения form-add scaffold:
- Module.bsl: пустые регионы вместо скелета процедуры ПриСозданииНаСервере
- Form.xml: убран <Events> (раньше привязывал OnCreateAtServer к процедуре)
- Form.xml: <SavedData>true</SavedData> теперь условный — ставится для
  Catalog/Document/etc (стандарт ERP, 99% форм), не ставится для
  DataProcessor/Report/External* (где у объекта нет состояния)

Это согласуется с workflow: form-compile перегенерирует Form.xml целиком,
поэтому привязки в scaffold могут стать orphan; пустые регионы +
без Events — корректная стартовая точка, которую form-edit/form-compile
наполняют атомарно.

Удалён навык epf-add-form (директория + тесты), вызовы заменены на
form-add в integration-тестах, в кейсах epf-validate/help-add, в
description epf-init/epf-bsp-init, в docs и README.

Перегенерированы snapshot'ы 5 навыков (form-add, form-compile,
form-edit, form-info, form-validate). Платформенная верификация в 1С 8.3.24
прошла для всех 9 кейсов form-add.

Bump form-add v1.3 → v1.4.
2026-04-25 15:26:54 +03:00
Nick Shirokov 2a86df1c98 refactor(skills): унифицировать стиль триггеров в description
Заменено «пользователь просит» → «нужно» в 12 навыках для согласования
с доминирующим стилем репозитория (cf-*, cfe-*, form-*, skd-*, mxl-*,
role-*, interface-*, subsystem-edit/info/validate уже используют «нужно»).

Дополнительно у db-list переформулирован триггер: вместо «"добавь базу"»
(что коллидирует с db-create) — «зарегистрировать базу в реестре»,
точнее отражает суть скилла (управление .v8-project.json).

Затронуто: db-create, db-dump-cf, db-dump-xml, db-list, db-load-cf,
db-load-git, db-load-xml, db-run, db-update, meta-compile, meta-remove,
subsystem-compile.
2026-04-25 13:35:44 +03:00
Nick Shirokov 2e1487fd17 feat(skills): добавить триггерные фразы в description 8 навыков
Навыки, у которых description содержал только «что делает» без условия
«когда использовать»: epf-init, erf-init, form-add, template-add, epf-add-form,
epf-bsp-init, epf-bsp-add-command, img-grid.

Добавлено второе предложение в стиле репозитория («Используй когда нужно …»).
Для epf-bsp-* уточнено назначение через ключевые термины БСП
(СведенияОВнешнейОбработке, «Дополнительные отчёты и обработки»).

Co-authored-by: Serg2000Mr <129394542+Serg2000Mr@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 13:29:20 +03:00
Nick Shirokov 3729b63b89 feat(skd-compile): @autoDates — дефолты use=Always + denyIncompleteValues=true
Производные &НачалоПериода/&КонецПериода требуют заполненный период,
поэтому сам параметр теперь по умолчанию получает use=Always и
denyIncompleteValues=true. В объектной форме явные значения перекрывают.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 12:24:22 +03:00
Nick Shirokov c3ec51e174 feat(template-add): автопоиск SrcDir по стандартным подпапкам
Если <SrcDir>/<ObjectName>.xml не найден — сканирует Reports,
DataProcessors, Documents, Catalogs и другие папки типа объектов.
При 1 совпадении расширяет SrcDir, при нескольких — ошибка со списком.
Попутно — уточнение описания SrcDir, обезличенный пример, флаг
-SetMainSKD в PS-стиле.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 11:51:07 +03:00
Nick Shirokov fb58a04700 feat(web-test): getFormState — полные reportSettings + chip-значения
На DCS-формах возвращались только настройки с явным чекбоксом «Использование» — остальные (всегда включённые) отбрасывались и пропадали из fields[]. Reference-поля с chip-контролом возвращали пустое value, потому что значение живёт в .chipsItem .chipsTitle, а не в input.value.

- DCS-группировка больше не требует наличия «Использование»; при его отсутствии setting.enabled = true (настройка всегда активна)
- При чтении input.value делается fallback на .chipsItem .chipsTitle в LABEL-родителе — через запятую, если значений несколько (первый элемент + «+N» при свёртке в UI)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 17:18:48 +03:00
Nick Shirokov a30b720c89 docs(skd-compile): SKILL.md — упростить описание @autoDates и dataParameters auto
Убрать XML-детали (useRestriction, xsi:type, <use>false</use>, <value xsi:nil>);
описывать поведение с точки зрения автора СКД, а не внутреннего представления.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 16:17:16 +03:00
Nick Shirokov 54d47aadad feat(skd-compile): dataParameters auto — копирование value всех типов (ЕРП-паттерн)
Раньше "auto" копировал только variant для StandardPeriod, остальные типы
теряли значение по умолчанию. Теперь:

- value задан (не-Custom для StandardPeriod) → value + use=true (implicit),
  правильный xsi:type: boolean/decimal/dateTime/string, DesignTimeValue для
  ссылочных типов.
- value отсутствует или StandardPeriod=Custom → <use>false</use>
  + <value xsi:nil="true"/>.

Соответствует тому, как 1С Designer и ЕРП-отчёты персистят
SettingsParameterValue. Тест auto-data-parameters расширен покрытием
decimal/string/ref/nil.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 16:13:04 +03:00
Nick Shirokov 3f23be8219 feat(skd-compile): @autoDates — НачалоПериода/КонецПериода вместо ДатаНачала/ДатаОкончания
Канонический паттерн БСП в Титан/ЕРП-отчётах использует имена
НачалоПериода/КонецПериода (~10:1 по частоте). Выражения
&Период.ДатаНачала/&Период.ДатаОкончания сохранены — это обращение
к внутренним полям StandardPeriod.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 15:51:22 +03:00
Nick Shirokov f5dd677ac8 fix(tests): cf-info/config-with-objects — preRun через meta-compile
Тест сломался с 0d5d345 (ужесточение cf-edit add-childObject: теперь требует, чтобы файл объекта существовал на диске). Там были пофикшены 4 теста cf-edit, но этот кейс cf-info с тем же паттерном в preRun пропустили.

Заменил cf-edit add-childObject на три meta-compile (Catalog.Товары, Document.Заказ, Enum.Статусы) — те сами регистрируют объекты в Configuration.xml и создают файлы. Snapshot перегенерирован.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 14:08:37 +03:00
Nick Shirokov 02e9053d00 feat(cf-validate,cfe-validate,epf-validate): поддержка платформы 8.5 (issue #13)
- `CompatibilityMode`, `ConfigurationExtensionCompatibilityMode`: добавлен `Version8_5_1`
- `InterfaceCompatibilityMode`: расширен до полного списка из 7 значений (Version8_2, Version8_2EnableTaxi, Taxi, TaxiEnableVersion8_2, TaxiEnableVersion8_5, Version8_5EnableTaxi, Version8_5) — заодно учтены недостающие 8.2-значения
- Принимается `version="2.21"` в заголовке MetaDataObject
- cf-edit/reference.md: обновлена таблица допустимых значений

Genrators (form-compile, form-add, cfe-borrow и др.) уже подхватывают версию формата через Detect-FormatVersion — не трогаем.

Closes #13

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 14:08:27 +03:00
Nick Shirokov bd462f4cc3 fix(web-test): hasVisibleModal — handle duplicate #modalSurface
1С оставляет стейл-элемент #modalSurface (display:none) после закрытия
формы и создаёт второй при открытии новой модалки — в DOM оказывается два
элемента с одинаковым id. getElementById возвращал первый (скрытый), из-за
чего detectForm/detectForms не видели активную модалку: getFormState
выдавал form+buttons от родительской формы, а clickElement кликал мимо
или падал.

Сканируем все #modalSurface через querySelectorAll и берём первый с
offsetWidth > 0.

Воспроизводилось стабильно на СКД-расшифровке после открытия "Настройки..."
в форме отчёта.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-16 21:24:44 +03:00
Nick Shirokov be74d224be feat(skd-compile): dataParameters auto — наследовать variant для StandardPeriod
Для параметров типа StandardPeriod в режиме "dataParameters": "auto" эмитируется <dcscor:value> с variant из дефолта параметра (Custom, если не задан) — как это делает 1C Designer при сохранении SettingsParameterValue для периодов.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-16 21:00:58 +03:00
Nick Shirokov 1b46eb4d85 feat(skd-compile): parameters — title и presentation как синонимы
- parameter принимает presentation как синоним title (1C UI показывает
  подпись параметра как "Представление" — модель по аналогии пишет presentation)
- availableValues[] принимает title как синоним presentation (обратная
  ошибка: модель пишет title по аналогии с самим параметром)

Обе формы пишутся в один и тот же XML-узел. Версии: skd-compile v1.13 → v1.14.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-16 20:37:00 +03:00
Nick Shirokov 1cea9e794e feat(skd-compile,skd-edit): calculatedFields — shorthand и объектные синонимы
- skd-compile v1.13: Parse-CalcShorthand теперь понимает "[Title]:type=expr#flags"
  (синхронно со skd-edit). Emit-CalcFields принимает name как синоним
  field/dataPath и строковую форму useRestriction ("#noField #noFilter ...").
- skd-edit v1.11: #restrict парсится по known-names pattern — исключает ложные
  срабатывания на # внутри строковых литералов в выражении.

Закрывает три ловушки из upload/bug-skd-compile-calculated-field-datapath.md,
где модель писала name вместо field и строковый useRestriction по аналогии
с shorthand-флагами.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-16 20:31:14 +03:00
Nick Shirokov 00fafd4af5 fix(web-test): scanSpreadsheetCells — use contentFrame() instead of index-based frame mapping
Replace fragile page.frames()[iframeIdx + 1] with handle.contentFrame() for
reliable iframe-to-Playwright-Frame resolution. The old index arithmetic could
break when 1C web client accumulates extra frames during prolonged sessions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 18:15:05 +03:00
Nick Shirokov e30b518935 feat(skd-info): auto-resolve object directory path to DCS template
When model passes report/dataprocessor path instead of template path,
scan Templates/*.xml metadata for DataCompositionSchema type and
auto-resolve. Single match → resolve with [i] hint, multiple → list.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 17:23:56 +03:00
681 changed files with 16303 additions and 2048 deletions
+32
View File
@@ -0,0 +1,32 @@
{
"name": "cc-1c-skills",
"interface": {
"displayName": "1C Skills"
},
"plugins": [
{
"name": "1c-skills",
"source": {
"source": "url",
"url": "https://github.com/Nikolay-Shirokov/cc-1c-skills.git",
"ref": "port-codex"
},
"policy": {
"installation": "AVAILABLE"
},
"category": "Development"
},
{
"name": "1c-skills-py",
"source": {
"source": "url",
"url": "https://github.com/Nikolay-Shirokov/cc-1c-skills.git",
"ref": "port-codex-py"
},
"policy": {
"installation": "AVAILABLE"
},
"category": "Development"
}
]
}
+24
View File
@@ -0,0 +1,24 @@
{
"$schema": "https://json.schemastore.org/claude-code-marketplace-manifest.json",
"name": "cc-1c-skills",
"description": "Маркетплейс навыков для разработки на платформе 1С:Предприятие",
"owner": {
"name": "Nikolay Shirokov"
},
"plugins": [
{
"name": "1c-skills",
"source": "./",
"description": "[PowerShell] Навыки для разработки на 1С:Предприятие 8.3 — абстракции над XML-форматами и CLI конфигуратора, плюс глаза и руки для тестирования через веб-клиент."
},
{
"name": "1c-skills-py",
"source": {
"source": "github",
"repo": "Nikolay-Shirokov/cc-1c-skills",
"ref": "port-claude-code-py"
},
"description": "[Python] То же — для Linux/Mac или когда PowerShell недоступен."
}
]
}
+31
View File
@@ -0,0 +1,31 @@
{
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
"name": "1c-skills",
"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": "./.claude/skills/"
}
+9 -7
View File
@@ -1,6 +1,6 @@
---
name: cf-edit
description: Точечное редактирование конфигурации 1С. Используй когда нужно изменить свойства конфигурации, добавить или удалить объект из состава, настроить роли по умолчанию
description: Точечное редактирование конфигурации 1С. Используй когда нужно изменить свойства конфигурации, добавить или удалить объект из состава, настроить роли по умолчанию, поменять раскладку панелей, настроить начальную страницу
argument-hint: -ConfigPath <path> -Operation <op> -Value <value>
allowed-tools:
- Bash
@@ -24,7 +24,7 @@ allowed-tools:
| `NoValidate` | Пропустить авто-валидацию |
```powershell
powershell.exe -NoProfile -File .claude/skills/cf-edit/scripts/cf-edit.ps1 -ConfigPath '<path>' -Operation modify-property -Value 'Version=1.0.0.1'
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-edit.ps1" -ConfigPath '<path>' -Operation modify-property -Value 'Version=1.0.0.1'
```
## Операции
@@ -37,6 +37,8 @@ powershell.exe -NoProfile -File .claude/skills/cf-edit/scripts/cf-edit.ps1 -Conf
| `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)
@@ -44,15 +46,15 @@ powershell.exe -NoProfile -File .claude/skills/cf-edit/scripts/cf-edit.ps1 -Conf
```powershell
# Изменить версию и поставщика
... -ConfigPath test-tmp/cf -Operation modify-property -Value "Version=1.0.0.1 ;; Vendor=Фирма 1С"
... -ConfigPath src -Operation modify-property -Value "Version=1.0.0.1 ;; Vendor=Фирма 1С"
# Добавить объекты
... -ConfigPath test-tmp/cf -Operation add-childObject -Value "Catalog.Товары ;; Document.Заказ"
... -ConfigPath src -Operation add-childObject -Value "Catalog.Товары ;; Document.Заказ"
# Удалить объект
... -ConfigPath test-tmp/cf -Operation remove-childObject -Value "Catalog.Устаревший"
... -ConfigPath src -Operation remove-childObject -Value "Catalog.Устаревший"
# Роли по умолчанию
... -ConfigPath test-tmp/cf -Operation add-defaultRole -Value "ПолныеПрава"
... -ConfigPath test-tmp/cf -Operation set-defaultRoles -Value "ПолныеПрава ;; Администратор"
... -ConfigPath src -Operation add-defaultRole -Value "ПолныеПрава"
... -ConfigPath src -Operation set-defaultRoles -Value "ПолныеПрава ;; Администратор"
```
+96 -13
View File
@@ -13,7 +13,7 @@
### Enum
| Свойство | Допустимые значения |
|----------|---------------------|
| `CompatibilityMode` | `Version8_3_20` ... `Version8_3_27`, `DontUse` |
| `CompatibilityMode` | `Version8_3_20` ... `Version8_3_28`, `Version8_5_1`, `DontUse` |
| `ConfigurationExtensionCompatibilityMode` | то же |
| `DefaultRunMode` | `ManagedApplication`, `OrdinaryApplication`, `Auto` |
| `ScriptVariant` | `Russian`, `English` |
@@ -21,7 +21,7 @@
| `ObjectAutonumerationMode` | `NotAutoFree`, `AutoFree` |
| `ModalityUseMode` | `DontUse`, `Use`, `UseWithWarnings` |
| `SynchronousPlatformExtensionAndAddInCallUseMode` | `DontUse`, `Use`, `UseWithWarnings` |
| `InterfaceCompatibilityMode` | `Taxi`, `TaxiEnableVersion8_2`, `Version8_2` |
| `InterfaceCompatibilityMode` | `Version8_2`, `Version8_2EnableTaxi`, `Taxi`, `TaxiEnableVersion8_2`, `TaxiEnableVersion8_5`, `Version8_5EnableTaxi`, `Version8_5` |
| `DatabaseTablespacesUseMode` | `DontUse`, `Use` |
| `MainClientApplicationWindowMode` | `Normal`, `Fullscreen`, `Kiosk` |
@@ -35,14 +35,7 @@
Формат: `Type.Name` — XML-тип и имя объекта через точку.
**Важно про `add-childObject`**: операция регистрирует в `<ChildObjects>` Configuration.xml только объект, **файл которого уже существует на диске** (например `Catalogs/Товары.xml`). Если файла нет — скрипт падает с exit 1 и подсказкой. Для создания нового объекта используй профильный навык — `/meta-compile` (Catalog, Document, Enum, Report, регистры и т.д.), `/role-compile` (Role), `/subsystem-compile` (Subsystem). Они создают файл И регистрируют его в Configuration.xml за один вызов.
Когда `add-childObject` всё-таки нужен: откатили Configuration.xml (или перезаписали из выгрузки БД), а файлы объектов остались — нужно восстановить ссылки в `<ChildObjects>`.
При добавлении объект вставляется в каноническую позицию:
1. Находит последний элемент того же типа → вставляет после
2. Если тип отсутствует → находит последний элемент предшествующего типа → вставляет после
3. Внутри одного типа — алфавитный порядок
**Важно про `add-childObject`**: регистрирует в `<ChildObjects>` объект, **файл которого уже существует на диске**. Если файла нет — exit 1. Для создания нового объекта используй профильный навык — `/meta-compile` (Catalog, Document, Enum, Report, регистры и т.д.), `/role-compile` (Role), `/subsystem-compile` (Subsystem). Они создают файл И регистрируют его за один вызов.
Batch: `"Catalog.Товары ;; Document.Заказ ;; Enum.ВидыОплат"`
@@ -52,6 +45,99 @@ Batch: `"Catalog.Товары ;; Document.Заказ ;; Enum.ВидыОплат"
`set-defaultRoles` полностью заменяет список ролей.
## set-panels
Перезаписывает `Ext/ClientApplicationInterface.xml` — раскладку панелей рабочего пространства Taxi. Файл создаётся с нуля; то, что не упомянуто в `value`, отсутствует на экране.
`value` — объект с ключами `top`, `left`, `right`, `bottom`. Каждый ключ — массив записей. Ключ можно опустить (= пустая сторона).
**Запись** — одна из:
- Строка-алиас (одна панель в этом слоте)
- Объект `{"group": [...]}` (стек: панели/подгруппы внутри располагаются друг под другом)
**Алиасы панелей:**
| Алиас | Панель |
|-------|--------|
| `sections` | Панель разделов |
| `open` | Панель открытых |
| `favorites` | Панель избранного |
| `history` | Панель истории |
| `functions` | Панель функций текущего раздела |
**Семантика:**
- Несколько записей в одной стороне → отдельные слоты «рядом» (несколько тегов `<top>`/...)
- `{"group":[...]}` → один тег с `<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-имена) — оба работают.
**Запись формы** — одна из:
- Строка `"<form>"` — только имя формы, дефолты `height=10`, `visibility=true`
- Объект `{form, height?, visibility?, roles?}`
| Поле | Канонич. | Дефолт | Описание |
|------|----------|--------|----------|
| `form` | `Form` | — | `CommonForm.X` или `Type.Object.Form.Name` (или UUID) |
| `height` | `Height` | `10` | Высота |
| `visibility` | `Visibility` | `true` | Общая видимость (`<xr:Common>`) |
| `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
@@ -62,6 +148,3 @@ Batch: `"Catalog.Товары ;; Document.Заказ ;; Enum.ВидыОплат"
]
```
## Авто-валидация
После сохранения автоматически запускается `cf-validate` (если не указан `-NoValidate`).
+316 -10
View File
@@ -1,9 +1,9 @@
# cf-edit v1.1 — Edit 1C configuration root (Configuration.xml)
# cf-edit v1.4 — Edit 1C configuration root (Configuration.xml)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)][string]$ConfigPath,
[Parameter(Mandatory)][Alias('Path')][string]$ConfigPath,
[string]$DefinitionFile,
[ValidateSet("modify-property","add-childObject","remove-childObject","add-defaultRole","remove-defaultRole","set-defaultRoles")]
[ValidateSet("modify-property","add-childObject","remove-childObject","add-defaultRole","remove-defaultRole","set-defaultRoles","set-panels","set-home-page")]
[string]$Operation,
[string]$Value,
[switch]$NoValidate
@@ -444,6 +444,308 @@ function Do-RemoveDefaultRole([string]$batchVal) {
}
}
# --- 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 -> <panel><uuid>...</uuid></panel>
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<panel id=`"$instId`">`r`n$indent`t<uuid>$u</uuid>`r`n$indent</panel>"
}
# Object {group: [...]} -> <group id=""><group><panel/></group>...</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<group>`r`n$childXml`r`n$indent`t</group>`r`n"
}
return "$indent<group id=`"$gid`">`r`n$inner$indent</group>"
}
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</$side>"
}
}
# 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 = @"
<panelDef id="b553047f-c9aa-4157-978d-448ecad24248"/>
<panelDef id="13322b22-3960-4d68-93a6-fe2dd7f28ca3"/>
<panelDef id="c933ac92-92cd-459d-81cc-e0c8a83ced99"/>
<panelDef id="cbab57f2-a0f3-4f0a-89ea-4cb19570ab75"/>
<panelDef id="b2735bd3-d822-4430-ba59-c9e869693b24"/>
"@
$bodyBlock = if ($body) { "$body`r`n" } else { "" }
$caiXml = @"
<?xml version="1.0" encoding="UTF-8"?>
<ClientApplicationInterface xmlns="http://v8.1c.ru/8.2/managed-application/core" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="InterfaceLayouter">
$bodyBlock$declarations
</ClientApplicationInterface>
"@
$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<xr:Common>$($common.ToString().ToLower())</xr:Common>"
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<xr:Value name=`"$escName`">$rval</xr:Value>"
}
}
$visBlock = $visParts -join "`r`n"
$escForm = [System.Security.SecurityElement]::Escape($formRef)
return @"
$indent<Item>
$indent`t<Form>$escForm</Form>
$indent`t<Height>$height</Height>
$indent`t<Visibility>
$visBlock
$indent`t</Visibility>
$indent</Item>
"@
}
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</$tag>"
}
$leftXml = Build-Column "LeftColumn" $leftItems
$rightXml = Build-Column "RightColumn" $rightItems
$hpXml = @"
<?xml version="1.0" encoding="UTF-8"?>
<HomePageWorkArea xmlns="http://v8.1c.ru/8.3/xcf/extrnprops" 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">
<WorkingAreaTemplate>$tmpl</WorkingAreaTemplate>
$leftXml
$rightXml
</HomePageWorkArea>
"@
$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
@@ -508,15 +810,19 @@ if ($DefinitionFile) {
foreach ($op in $operations) {
$opName = if ($op.operation) { "$($op.operation)" } else { "$Operation" }
$opValue = if ($op.value) { "$($op.value)" } else { "$Value" }
# 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 $opValue }
"add-childObject" { Do-AddChildObject $opValue }
"remove-childObject" { Do-RemoveChildObject $opValue }
"add-defaultRole" { Do-AddDefaultRole $opValue }
"remove-defaultRole" { Do-RemoveDefaultRole $opValue }
"set-defaultRoles" { Do-SetDefaultRoles $opValue }
"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 }
}
}
+278 -10
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# cf-edit v1.1 — Edit 1C configuration root (Configuration.xml)
# cf-edit v1.4 — Edit 1C configuration root (Configuration.xml)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -7,6 +7,7 @@ import json
import os
import subprocess
import sys
import uuid as _uuid
from html import escape as html_escape
from lxml import etree
@@ -159,9 +160,9 @@ 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", required=True)
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"])
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()
@@ -493,6 +494,269 @@ def main():
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}<panel id="{inst}">\r\n{indent}\t<uuid>{PANEL_UUIDS[key]}</uuid>\r\n{indent}</panel>'
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<group>\r\n{child_xml}\r\n{indent}\t</group>\r\n"
return f'{indent}<group id="{gid}">\r\n{inner}{indent}</group>'
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</{side}>")
body = "\r\n".join(body_parts)
body_block = body + "\r\n" if body else ""
declarations = (
'\t<panelDef id="b553047f-c9aa-4157-978d-448ecad24248"/>\r\n'
'\t<panelDef id="13322b22-3960-4d68-93a6-fe2dd7f28ca3"/>\r\n'
'\t<panelDef id="c933ac92-92cd-459d-81cc-e0c8a83ced99"/>\r\n'
'\t<panelDef id="cbab57f2-a0f3-4f0a-89ea-4cb19570ab75"/>\r\n'
'\t<panelDef id="b2735bd3-d822-4430-ba59-c9e869693b24"/>'
)
cai_xml = (
'<?xml version="1.0" encoding="UTF-8"?>\r\n'
'<ClientApplicationInterface xmlns="http://v8.1c.ru/8.2/managed-application/core" '
'xmlns:xs="http://www.w3.org/2001/XMLSchema" '
'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" '
'xsi:type="InterfaceLayouter">\r\n'
f'{body_block}{declarations}\r\n'
'</ClientApplicationInterface>'
)
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<xr:Common>{str(common).lower()}</xr:Common>"]
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<xr:Value name="{html_escape(rname, quote=True)}">{rval_s}</xr:Value>')
vis_block = "\r\n".join(vis_parts)
esc_form = html_escape(form_ref, quote=True)
return (
f"{indent}<Item>\r\n"
f"{indent}\t<Form>{esc_form}</Form>\r\n"
f"{indent}\t<Height>{height}</Height>\r\n"
f"{indent}\t<Visibility>\r\n"
f"{vis_block}\r\n"
f"{indent}\t</Visibility>\r\n"
f"{indent}</Item>"
)
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</{tag}>"
left_xml = build_column("LeftColumn", left_items)
right_xml = build_column("RightColumn", right_items)
hp_xml = (
'<?xml version="1.0" encoding="UTF-8"?>\r\n'
'<HomePageWorkArea xmlns="http://v8.1c.ru/8.3/xcf/extrnprops" '
'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">\r\n'
f'\t<WorkingAreaTemplate>{tmpl}</WorkingAreaTemplate>\r\n'
f'{left_xml}\r\n'
f'{right_xml}\r\n'
'</HomePageWorkArea>'
)
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:
@@ -513,17 +777,21 @@ def main():
op_value = op.get("value", args.Value or "")
if op_name == "modify-property":
do_modify_property(op_value)
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)
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)
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)
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)
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)
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)
@@ -538,7 +806,7 @@ def main():
if os.path.isfile(validate_script):
print()
print("--- Running cf-validate ---")
subprocess.run([sys.executable, validate_script, "-ConfigPath", resolved_path])
subprocess.run([sys.executable, validate_script, "-ConfigPath", "-Path", resolved_path])
# --- Summary ---
print()
+10 -6
View File
@@ -1,7 +1,7 @@
---
name: cf-info
description: Анализ структуры конфигурации 1С — свойства, состав, счётчики объектов. Используй для обзора конфигурации — какие объекты есть, сколько их, какие настройки
argument-hint: <ConfigPath> [-Mode overview|brief|full]
argument-hint: <ConfigPath> [-Mode overview|brief|full] [-Section home-page]
allowed-tools:
- Bash
- Read
@@ -18,11 +18,12 @@ allowed-tools:
|----------|----------|
| `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 -File .claude/skills/cf-info/scripts/cf-info.ps1 -ConfigPath "<путь>"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-info.ps1" -ConfigPath "<путь>"
```
## Три режима
@@ -37,14 +38,17 @@ powershell.exe -NoProfile -File .claude/skills/cf-info/scripts/cf-info.ps1 -Conf
```powershell
# Обзор пустой конфигурации
... -ConfigPath upload/cfempty
... -ConfigPath src
# Краткая сводка реальной конфигурации
... -ConfigPath upload/acc_8.3.24 -Mode brief
... -ConfigPath src -Mode brief
# Полная информация
... -ConfigPath upload/acc_8.3.24 -Mode full
... -ConfigPath src -Mode full
# С пагинацией
... -ConfigPath upload/acc_8.3.24 -Mode full -Limit 50 -Offset 100
... -ConfigPath src -Mode full -Limit 50 -Offset 100
# Drill-down: только начальная страница (раскладка форм с ролями)
... -ConfigPath src -Section home-page
```
+197 -5
View File
@@ -1,9 +1,12 @@
# cf-info v1.0 — Compact summary of 1C configuration root
# cf-info v1.2 — Compact summary of 1C configuration root
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory=$true)][string]$ConfigPath,
[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
@@ -118,6 +121,115 @@ $typeRuNames = @{
"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
@@ -154,7 +266,7 @@ $cfgDbSpaces = Get-PropText "DatabaseTablespacesUseMode"
$cfgWindowMode = Get-PropText "MainClientApplicationWindowMode"
# --- BRIEF mode ---
if ($Mode -eq "brief") {
if ($Mode -eq "brief" -and -not $Section) {
$synPart = if ($cfgSynonym) { " $dash `"$cfgSynonym`"" } else { "" }
$verPart = if ($cfgVersion) { " v$cfgVersion" } else { "" }
$compatPart = if ($cfgCompat) { " | $cfgCompat" } else { "" }
@@ -162,7 +274,7 @@ if ($Mode -eq "brief") {
}
# --- OVERVIEW mode ---
if ($Mode -eq "overview") {
if ($Mode -eq "overview" -and -not $Section) {
$synPart = if ($cfgSynonym) { " $dash `"$cfgSynonym`"" } else { "" }
$verPart = if ($cfgVersion) { " v$cfgVersion" } else { "" }
Out "=== Конфигурация: ${cfgName}${synPart}${verPart} ==="
@@ -181,6 +293,33 @@ if ($Mode -eq "overview") {
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 ""
@@ -203,8 +342,34 @@ if ($Mode -eq "overview") {
}
}
# --- 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") {
if ($Mode -eq "full" -and -not $Section) {
$synPart = if ($cfgSynonym) { " $dash `"$cfgSynonym`"" } else { "" }
$verPart = if ($cfgVersion) { " v$cfgVersion" } else { "" }
Out "=== Конфигурация: ${cfgName}${synPart}${verPart} ==="
@@ -275,6 +440,33 @@ if ($Mode -eq "full") {
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")
+165 -5
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# cf-info v1.0 — Compact summary of 1C configuration root
# cf-info v1.2 — Compact summary of 1C configuration root
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -13,8 +13,9 @@ sys.stderr.reconfigure(encoding="utf-8")
# --- Argument parsing ---
parser = argparse.ArgumentParser(description="Analyze 1C configuration structure", allow_abbrev=False)
parser.add_argument("-ConfigPath", required=True, help="Path to Configuration.xml or directory")
parser.add_argument("-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")
@@ -125,6 +126,108 @@ type_ru_names = {
"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
@@ -159,14 +262,14 @@ cfg_db_spaces = get_prop_text("DatabaseTablespacesUseMode")
cfg_window_mode = get_prop_text("MainClientApplicationWindowMode")
# --- BRIEF mode ---
if args.Mode == "brief":
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":
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} ===")
@@ -187,6 +290,20 @@ if args.Mode == "overview":
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()
@@ -207,7 +324,30 @@ if args.Mode == "overview":
out(f" {padded} {count}")
# --- FULL mode ---
if args.Mode == "full":
# --- 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} ===")
@@ -283,6 +423,26 @@ if args.Mode == "full":
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 = [
+1 -1
View File
@@ -24,7 +24,7 @@ allowed-tools:
| `CompatibilityMode` | Режим совместимости (default: `Version8_3_24`) |
```powershell
powershell.exe -NoProfile -File .claude/skills/cf-init/scripts/cf-init.ps1 -Name "МояКонфигурация"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-init.ps1" -Name "МояКонфигурация"
```
## Примеры
+35 -1
View File
@@ -1,4 +1,4 @@
# cf-init v1.1 — Create empty 1C configuration scaffold
# cf-init v1.2 — Create empty 1C configuration scaffold
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -192,6 +192,33 @@ $langXml = @"
</MetaDataObject>
"@
# --- 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 = @"
<?xml version="1.0" encoding="UTF-8"?>
<ClientApplicationInterface xmlns="http://v8.1c.ru/8.2/managed-application/core" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="InterfaceLayouter">
<top>
<panel id="$openPanelInst">
<uuid>cbab57f2-a0f3-4f0a-89ea-4cb19570ab75</uuid>
</panel>
</top>
<left>
<panel id="$sectionsPanelInst">
<uuid>b553047f-c9aa-4157-978d-448ecad24248</uuid>
</panel>
</left>
<panelDef id="b553047f-c9aa-4157-978d-448ecad24248"/>
<panelDef id="13322b22-3960-4d68-93a6-fe2dd7f28ca3"/>
<panelDef id="c933ac92-92cd-459d-81cc-e0c8a83ced99"/>
<panelDef id="cbab57f2-a0f3-4f0a-89ea-4cb19570ab75"/>
<panelDef id="b2735bd3-d822-4430-ba59-c9e869693b24"/>
</ClientApplicationInterface>
"@
# --- Create directories ---
if (-not (Test-Path $OutputDir)) {
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
@@ -200,6 +227,10 @@ $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)
@@ -207,9 +238,12 @@ $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"
+31 -1
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# cf-init v1.1 — Create empty 1C configuration scaffold
# 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
@@ -184,20 +184,50 @@ def main():
\t</Language>
</MetaDataObject>'''
# --- 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'''<?xml version="1.0" encoding="UTF-8"?>
<ClientApplicationInterface xmlns="http://v8.1c.ru/8.2/managed-application/core" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="InterfaceLayouter">
\t<top>
\t\t<panel id="{open_panel_inst}">
\t\t\t<uuid>cbab57f2-a0f3-4f0a-89ea-4cb19570ab75</uuid>
\t\t</panel>
\t</top>
\t<left>
\t\t<panel id="{sections_panel_inst}">
\t\t\t<uuid>b553047f-c9aa-4157-978d-448ecad24248</uuid>
\t\t</panel>
\t</left>
\t<panelDef id="b553047f-c9aa-4157-978d-448ecad24248"/>
\t<panelDef id="13322b22-3960-4d68-93a6-fe2dd7f28ca3"/>
\t<panelDef id="c933ac92-92cd-459d-81cc-e0c8a83ced99"/>
\t<panelDef id="cbab57f2-a0f3-4f0a-89ea-4cb19570ab75"/>
\t<panelDef id="b2735bd3-d822-4430-ba59-c9e869693b24"/>
</ClientApplicationInterface>'''
# --- 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()
+2 -2
View File
@@ -24,6 +24,6 @@ allowed-tools:
## Команда
```powershell
powershell.exe -NoProfile -File .claude/skills/cf-validate/scripts/cf-validate.ps1 -ConfigPath "upload/cfempty"
powershell.exe -NoProfile -File .claude/skills/cf-validate/scripts/cf-validate.ps1 -ConfigPath "upload/cfempty/Configuration.xml"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-validate.ps1" -ConfigPath "upload/cfempty"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-validate.ps1" -ConfigPath "upload/cfempty/Configuration.xml"
```
@@ -1,7 +1,8 @@
# cf-validate v1.1 — Validate 1C configuration root structure
# 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,
@@ -145,17 +146,17 @@ $childTypeDirMap = @{
# 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")
"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" = @("Taxi","TaxiEnableVersion8_2","Version8_2")
"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")
"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 ---
@@ -203,8 +204,8 @@ if ($root.NamespaceURI -ne $expectedNs) {
$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") {
Report-Warn "1. Unusual version '$version' (expected 2.17 or 2.20)"
} 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
@@ -535,6 +536,72 @@ if ($childObjNode) {
}
}
# --- 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
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# cf-validate v1.1 — Validate 1C configuration XML structure
# 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
@@ -82,7 +82,7 @@ VALID_ENUM_VALUES = {
'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_3_26', 'Version8_3_27', 'Version8_3_28', 'Version8_5_1',
],
'DefaultRunMode': ['ManagedApplication', 'OrdinaryApplication', 'Auto'],
'ScriptVariant': ['Russian', 'English'],
@@ -90,7 +90,10 @@ VALID_ENUM_VALUES = {
'ObjectAutonumerationMode': ['NotAutoFree', 'AutoFree'],
'ModalityUseMode': ['DontUse', 'Use', 'UseWithWarnings'],
'SynchronousPlatformExtensionAndAddInCallUseMode': ['DontUse', 'Use', 'UseWithWarnings'],
'InterfaceCompatibilityMode': ['Taxi', 'TaxiEnableVersion8_2', 'Version8_2'],
'InterfaceCompatibilityMode': [
'Version8_2', 'Version8_2EnableTaxi', 'Taxi', 'TaxiEnableVersion8_2',
'TaxiEnableVersion8_5', 'Version8_5EnableTaxi', 'Version8_5',
],
'DatabaseTablespacesUseMode': ['DontUse', 'Use'],
'MainClientApplicationWindowMode': ['Normal', 'Fullscreen', 'Kiosk'],
'CompatibilityMode': [
@@ -100,7 +103,7 @@ VALID_ENUM_VALUES = {
'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_3_26', 'Version8_3_27', 'Version8_3_28', 'Version8_5_1',
],
}
@@ -162,7 +165,7 @@ def main():
parser = argparse.ArgumentParser(
description='Validate 1C configuration XML structure', allow_abbrev=False
)
parser.add_argument('-ConfigPath', dest='ConfigPath', required=True)
parser.add_argument('-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='')
@@ -228,8 +231,8 @@ def main():
version = root.get('version', '')
if not version:
r.warn('1. Missing version attribute on MetaDataObject')
elif version not in ('2.17', '2.20'):
r.warn(f"1. Unusual version '{version}' (expected 2.17 or 2.20)")
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
@@ -534,6 +537,60 @@ def main():
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)
+1 -1
View File
@@ -71,7 +71,7 @@ allowed-tools:
## Команда
```powershell
powershell.exe -NoProfile -File .claude/skills/cfe-borrow/scripts/cfe-borrow.ps1 -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-borrow.ps1" -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты"
```
## Примеры
+1 -1
View File
@@ -23,7 +23,7 @@ allowed-tools:
## Команда
```powershell
powershell.exe -NoProfile -File .claude/skills/cfe-diff/scripts/cfe-diff.ps1 -ExtensionPath src -ConfigPath C:\cfsrc\erp -Mode A
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-diff.ps1" -ExtensionPath src -ConfigPath C:\cfsrc\erp -Mode A
```
## Mode A — обзор расширения
+1 -1
View File
@@ -44,7 +44,7 @@ allowed-tools:
## Команда
```powershell
powershell.exe -NoProfile -File .claude/skills/cfe-init/scripts/cfe-init.ps1 -Name "МоёРасширение"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-init.ps1" -Name "МоёРасширение"
```
## Примеры
+1 -1
View File
@@ -51,7 +51,7 @@ allowed-tools:
## Команда
```powershell
powershell.exe -NoProfile -File .claude/skills/cfe-patch-method/scripts/cfe-patch-method.ps1 -ExtensionPath src -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-patch-method.ps1" -ExtensionPath src -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
```
## Примеры
+2 -2
View File
@@ -24,6 +24,6 @@ allowed-tools:
## Команда
```powershell
powershell.exe -NoProfile -File .claude/skills/cfe-validate/scripts/cfe-validate.ps1 -ExtensionPath "src"
powershell.exe -NoProfile -File .claude/skills/cfe-validate/scripts/cfe-validate.ps1 -ExtensionPath "src/Configuration.xml"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-validate.ps1" -ExtensionPath "src"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-validate.ps1" -ExtensionPath "src/Configuration.xml"
```
@@ -1,7 +1,8 @@
# cfe-validate v1.3 — Validate 1C configuration extension structure (CFE)
# 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,
@@ -145,10 +146,10 @@ $childTypeDirMap = @{
# 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")
"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" = @("Taxi","TaxiEnableVersion8_2","Version8_2")
"InterfaceCompatibilityMode" = @("Version8_2","Version8_2EnableTaxi","Taxi","TaxiEnableVersion8_2","TaxiEnableVersion8_5","Version8_5EnableTaxi","Version8_5")
}
# --- 1. Parse XML ---
@@ -196,8 +197,8 @@ if ($root.NamespaceURI -ne $expectedNs) {
$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") {
Report-Warn "1. Unusual version '$version' (expected 2.17 or 2.20)"
} 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
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# cfe-validate v1.3 — Validate 1C configuration extension XML structure (CFE)
# 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
@@ -82,11 +82,14 @@ VALID_ENUM_VALUES = {
'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_3_26', 'Version8_3_27', 'Version8_3_28', 'Version8_5_1',
],
'DefaultRunMode': ['ManagedApplication', 'OrdinaryApplication', 'Auto'],
'ScriptVariant': ['Russian', 'English'],
'InterfaceCompatibilityMode': ['Taxi', 'TaxiEnableVersion8_2', 'Version8_2'],
'InterfaceCompatibilityMode': [
'Version8_2', 'Version8_2EnableTaxi', 'Taxi', 'TaxiEnableVersion8_2',
'TaxiEnableVersion8_5', 'Version8_5EnableTaxi', 'Version8_5',
],
}
EXPECTED_NS = 'http://v8.1c.ru/8.3/MDClasses'
@@ -147,7 +150,7 @@ def main():
parser = argparse.ArgumentParser(
description='Validate 1C configuration extension XML structure (CFE)', allow_abbrev=False
)
parser.add_argument('-ExtensionPath', dest='ExtensionPath', required=True)
parser.add_argument('-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='')
@@ -213,8 +216,8 @@ def main():
version = root.get('version', '')
if not version:
r.warn('1. Missing version attribute on MetaDataObject')
elif version not in ('2.17', '2.20'):
r.warn(f"1. Unusual version '{version}' (expected 2.17 or 2.20)")
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
+6 -6
View File
@@ -1,6 +1,6 @@
---
name: db-create
description: Создание информационной базы 1С. Используй когда пользователь просит создать базу, новую ИБ, пустую базу
description: Создание информационной базы 1С. Используй когда нужно создать базу, новую ИБ, пустую базу
argument-hint: <path|name>
allowed-tools:
- Bash
@@ -31,7 +31,7 @@ allowed-tools:
## Команда
```powershell
powershell.exe -NoProfile -File .claude/skills/db-create/scripts/db-create.ps1 <параметры>
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" <параметры>
```
### Параметры скрипта
@@ -65,14 +65,14 @@ powershell.exe -NoProfile -File .claude/skills/db-create/scripts/db-create.ps1 <
```powershell
# Создать файловую базу
powershell.exe -NoProfile -File .claude/skills/db-create/scripts/db-create.ps1 -InfoBasePath "C:\Bases\NewDB"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" -InfoBasePath "C:\Bases\NewDB"
# Создать серверную базу
powershell.exe -NoProfile -File .claude/skills/db-create/scripts/db-create.ps1 -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test"
# Создать из шаблона CF
powershell.exe -NoProfile -File .claude/skills/db-create/scripts/db-create.ps1 -InfoBasePath "C:\Bases\NewDB" -UseTemplate "C:\Templates\config.cf"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" -InfoBasePath "C:\Bases\NewDB" -UseTemplate "C:\Templates\config.cf"
# Создать и добавить в список баз
powershell.exe -NoProfile -File .claude/skills/db-create/scripts/db-create.ps1 -InfoBasePath "C:\Bases\NewDB" -AddToList -ListName "Новая база"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" -InfoBasePath "C:\Bases\NewDB" -AddToList -ListName "Новая база"
```
+5 -5
View File
@@ -1,6 +1,6 @@
---
name: db-dump-cf
description: Выгрузка конфигурации 1С в CF-файл. Используй когда пользователь просит выгрузить конфигурацию в CF, сохранить конфигурацию, сделать бэкап CF
description: Выгрузка конфигурации 1С в CF-файл. Используй когда нужно выгрузить конфигурацию в CF, сохранить конфигурацию, сделать бэкап CF
argument-hint: "[database] [output.cf]"
allowed-tools:
- Bash
@@ -35,7 +35,7 @@ allowed-tools:
## Команда
```powershell
powershell.exe -NoProfile -File .claude/skills/db-dump-cf/scripts/db-dump-cf.ps1 <параметры>
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.ps1" <параметры>
```
### Параметры скрипта
@@ -69,11 +69,11 @@ powershell.exe -NoProfile -File .claude/skills/db-dump-cf/scripts/db-dump-cf.ps1
```powershell
# Выгрузка конфигурации (файловая база)
powershell.exe -NoProfile -File .claude/skills/db-dump-cf/scripts/db-dump-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "C:\backup\config.cf"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "C:\backup\config.cf"
# Серверная база
powershell.exe -NoProfile -File .claude/skills/db-dump-cf/scripts/db-dump-cf.ps1 -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -OutputFile "config.cf"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -OutputFile "config.cf"
# Выгрузка расширения
powershell.exe -NoProfile -File .claude/skills/db-dump-cf/scripts/db-dump-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "ext.cfe" -Extension "МоёРасширение"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "ext.cfe" -Extension "МоёРасширение"
```
+7 -7
View File
@@ -1,6 +1,6 @@
---
name: db-dump-xml
description: Выгрузка конфигурации 1С в XML-файлы. Используй когда пользователь просит выгрузить конфигурацию в файлы, XML, исходники, DumpConfigToFiles
description: Выгрузка конфигурации 1С в XML-файлы. Используй когда нужно выгрузить конфигурацию в файлы, XML, исходники, DumpConfigToFiles
argument-hint: "[database] [outputDir]"
allowed-tools:
- Bash
@@ -37,7 +37,7 @@ allowed-tools:
## Команда
```powershell
powershell.exe -NoProfile -File .claude/skills/db-dump-xml/scripts/db-dump-xml.ps1 <параметры>
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" <параметры>
```
### Параметры скрипта
@@ -81,17 +81,17 @@ powershell.exe -NoProfile -File .claude/skills/db-dump-xml/scripts/db-dump-xml.p
```powershell
# Полная выгрузка (файловая база)
powershell.exe -NoProfile -File .claude/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 -File "${CLAUDE_SKILL_DIR}/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 -File .claude/skills/db-dump-xml/scripts/db-dump-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Changes
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Changes
# Частичная выгрузка
powershell.exe -NoProfile -File .claude/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 -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Partial -Objects "Справочник.Номенклатура,Документ.Заказ"
# Серверная база
powershell.exe -NoProfile -File .claude/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 -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -ConfigDir "C:\WS\cfsrc" -Mode Full
# Выгрузка расширения
powershell.exe -NoProfile -File .claude/skills/db-dump-xml/scripts/db-dump-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\ext_src" -Mode Full -Extension "МоёРасширение"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\ext_src" -Mode Full -Extension "МоёРасширение"
```
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: db-list
description: Управление реестром баз данных 1С (.v8-project.json). Используй когда пользователь говорит про базы данных, список баз, "добавь базу", "какие базы есть"
description: Управление реестром баз данных 1С (.v8-project.json). Используй когда нужно работать с реестром баз список баз, зарегистрировать базу в реестре, какие базы есть
argument-hint: "[add|remove|show]"
allowed-tools:
- Read
+5 -5
View File
@@ -1,6 +1,6 @@
---
name: db-load-cf
description: Загрузка конфигурации 1С из CF-файла. Используй когда пользователь просит загрузить конфигурацию из CF, восстановить из бэкапа CF
description: Загрузка конфигурации 1С из CF-файла. Используй когда нужно загрузить конфигурацию из CF, восстановить из бэкапа CF
argument-hint: <input.cf> [database]
allowed-tools:
- Bash
@@ -36,7 +36,7 @@ allowed-tools:
## Команда
```powershell
powershell.exe -NoProfile -File .claude/skills/db-load-cf/scripts/db-load-cf.ps1 <параметры>
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.ps1" <параметры>
```
### Параметры скрипта
@@ -71,11 +71,11 @@ powershell.exe -NoProfile -File .claude/skills/db-load-cf/scripts/db-load-cf.ps1
```powershell
# Файловая база
powershell.exe -NoProfile -File .claude/skills/db-load-cf/scripts/db-load-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "C:\backup\config.cf"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "C:\backup\config.cf"
# Серверная база
powershell.exe -NoProfile -File .claude/skills/db-load-cf/scripts/db-load-cf.ps1 -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test" -UserName "Admin" -Password "secret" -InputFile "config.cf"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test" -UserName "Admin" -Password "secret" -InputFile "config.cf"
# Загрузка расширения
powershell.exe -NoProfile -File .claude/skills/db-load-cf/scripts/db-load-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "ext.cfe" -Extension "МоёРасширение"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "ext.cfe" -Extension "МоёРасширение"
```
+78 -78
View File
@@ -1,78 +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 -File .claude/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 <range>` | для Commit | Диапазон коммитов (напр. `HEAD~3..HEAD`) |
| `-Extension <имя>` | нет | Загрузить в расширение |
| `-AllExtensions` | нет | Загрузить все расширения |
| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` |
| `-DryRun` | нет | Только показать что будет загружено (без загрузки) |
| `-UpdateDB` | нет | После загрузки сразу обновить конфигурацию БД (`/UpdateDBCfg`) |
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
## После выполнения
1. Показать список загруженных файлов и результат из лога
2. Если `-UpdateDB` не был указан — **предложить `/db-update`** для применения изменений к БД
## Примеры
```powershell
# Все незафиксированные изменения
powershell.exe -NoProfile -File .claude/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 -File .claude/skills/db-load-git/scripts/db-load-git.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\WS\cfsrc" -Source Commit -CommitRange "HEAD~3..HEAD"
```
---
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 -File "${CLAUDE_SKILL_DIR}/scripts/db-load-git.ps1" <параметры>
```
### Параметры скрипта
| Параметр | Обязательный | Описание |
|----------|:------------:|----------|
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
| `-InfoBasePath <путь>` | * | Файловая база |
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
| `-UserName <имя>` | нет | Имя пользователя |
| `-Password <пароль>` | нет | Пароль |
| `-ConfigDir <путь>` | да | Каталог XML-выгрузки (git-репозиторий) |
| `-Source <источник>` | нет | `All` (по умолч.) / `Staged` / `Unstaged` / `Commit` |
| `-CommitRange <range>` | для Commit | Диапазон коммитов (напр. `HEAD~3..HEAD`) |
| `-Extension <имя>` | нет | Загрузить в расширение |
| `-AllExtensions` | нет | Загрузить все расширения |
| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` |
| `-DryRun` | нет | Только показать что будет загружено (без загрузки) |
| `-UpdateDB` | нет | После загрузки сразу обновить конфигурацию БД (`/UpdateDBCfg`) |
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
## После выполнения
1. Показать список загруженных файлов и результат из лога
2. Если `-UpdateDB` не был указан — **предложить `/db-update`** для применения изменений к БД
## Примеры
```powershell
# Все незафиксированные изменения
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/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 -File "${CLAUDE_SKILL_DIR}/scripts/db-load-git.ps1" -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\WS\cfsrc" -Source Commit -CommitRange "HEAD~3..HEAD"
```
+6 -6
View File
@@ -1,6 +1,6 @@
---
name: db-load-xml
description: Загрузка конфигурации 1С из XML-файлов. Используй когда пользователь просит загрузить конфигурацию из файлов, XML, исходников, LoadConfigFromFiles
description: Загрузка конфигурации 1С из XML-файлов. Используй когда нужно загрузить конфигурацию из файлов, XML, исходников, LoadConfigFromFiles
argument-hint: <configDir> [database]
allowed-tools:
- Bash
@@ -38,7 +38,7 @@ allowed-tools:
## Команда
```powershell
powershell.exe -NoProfile -File .claude/skills/db-load-xml/scripts/db-load-xml.ps1 <параметры>
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" <параметры>
```
### Параметры скрипта
@@ -96,14 +96,14 @@ Documents/Заказ/Forms/ФормаДокумента.xml
```powershell
# Полная загрузка
powershell.exe -NoProfile -File .claude/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 -File "${CLAUDE_SKILL_DIR}/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 -File .claude/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 -File "${CLAUDE_SKILL_DIR}/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 -File .claude/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 -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\ext_src" -Mode Full -Extension "МоёРасширение"
# Загрузка + обновление БД в одном запуске
powershell.exe -NoProfile -File .claude/skills/db-load-xml/scripts/db-load-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full -UpdateDB
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full -UpdateDB
```
+6 -6
View File
@@ -1,6 +1,6 @@
---
name: db-run
description: Запуск 1С:Предприятие. Используй когда пользователь просит запустить 1С, открыть базу, запустить предприятие
description: Запуск 1С:Предприятие. Используй когда нужно запустить 1С, открыть базу, запустить предприятие
argument-hint: "[database]"
allowed-tools:
- Bash
@@ -36,7 +36,7 @@ allowed-tools:
## Команда
```powershell
powershell.exe -NoProfile -File .claude/skills/db-run/scripts/db-run.ps1 <параметры>
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" <параметры>
```
### Параметры скрипта
@@ -63,14 +63,14 @@ powershell.exe -NoProfile -File .claude/skills/db-run/scripts/db-run.ps1 <пар
```powershell
# Простой запуск
powershell.exe -NoProfile -File .claude/skills/db-run/scripts/db-run.ps1 -InfoBasePath "C:\Bases\MyDB" -UserName "Admin"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin"
# Запуск с обработкой
powershell.exe -NoProfile -File .claude/skills/db-run/scripts/db-run.ps1 -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -Execute "C:\epf\МояОбработка.epf"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -Execute "C:\epf\МояОбработка.epf"
# Открыть по навигационной ссылке
powershell.exe -NoProfile -File .claude/skills/db-run/scripts/db-run.ps1 -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -URL "e1cib/data/Справочник.Номенклатура"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -URL "e1cib/data/Справочник.Номенклатура"
# Серверная база с параметром запуска
powershell.exe -NoProfile -File .claude/skills/db-run/scripts/db-run.ps1 -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -CParam "ЗапуститьОбновление"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -CParam "ЗапуститьОбновление"
```
+5 -5
View File
@@ -1,6 +1,6 @@
---
name: db-update
description: Обновление конфигурации базы данных 1С. Используй когда пользователь просит обновить БД, применить конфигурацию, UpdateDBCfg
description: Обновление конфигурации базы данных 1С. Используй когда нужно обновить БД, применить конфигурацию, UpdateDBCfg
argument-hint: "[database]"
allowed-tools:
- Bash
@@ -35,7 +35,7 @@ allowed-tools:
## Команда
```powershell
powershell.exe -NoProfile -File .claude/skills/db-update/scripts/db-update.ps1 <параметры>
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" <параметры>
```
### Параметры скрипта
@@ -83,11 +83,11 @@ powershell.exe -NoProfile -File .claude/skills/db-update/scripts/db-update.ps1 <
```powershell
# Обычное обновление (файловая база)
powershell.exe -NoProfile -File .claude/skills/db-update/scripts/db-update.ps1 -InfoBasePath "C:\Bases\MyDB" -UserName "Admin"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin"
# Динамическое обновление (серверная база)
powershell.exe -NoProfile -File .claude/skills/db-update/scripts/db-update.ps1 -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -Dynamic "+"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -Dynamic "+"
# Обновление расширения
powershell.exe -NoProfile -File .claude/skills/db-update/scripts/db-update.ps1 -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -Extension "МоёРасширение"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -Extension "МоёРасширение"
```
-60
View File
@@ -1,60 +0,0 @@
---
name: epf-add-form
description: Добавить управляемую форму к внешней обработке 1С
argument-hint: <ProcessorName> <FormName> [Synonym]
allowed-tools:
- Bash
- Read
- Write
- Edit
- Glob
- Grep
---
# /epf-add-form — Добавление формы
Создаёт управляемую форму и регистрирует её в корневом XML обработки.
## Usage
```
/epf-add-form <ProcessorName> <FormName> [Synonym] [--main]
```
| Параметр | Обязательный | По умолчанию | Описание |
|---------------|:------------:|--------------|-------------------------------------------|
| ProcessorName | да | — | Имя обработки (должна существовать) |
| FormName | да | — | Имя формы |
| Synonym | нет | = FormName | Синоним формы |
| --main | нет | авто | Установить как форму по умолчанию (автоматически для первой формы) |
| SrcDir | нет | `src` | Каталог исходников |
## Команда
```powershell
powershell.exe -NoProfile -File .claude/skills/epf-add-form/scripts/add-form.ps1 -ProcessorName "<ProcessorName>" -FormName "<FormName>" [-Synonym "<Synonym>"] [-Main] [-SrcDir "<SrcDir>"]
```
## Что создаётся
```
<SrcDir>/<ProcessorName>/Forms/
├── <FormName>.xml # Метаданные формы (1 UUID)
└── <FormName>/
└── Ext/
├── Form.xml # Описание формы (logform namespace)
└── Form/
└── Module.bsl # BSL-модуль с 4 регионами
```
## Что модифицируется
- `<SrcDir>/<ProcessorName>.xml` — добавляется `<Form>` в `ChildObjects`, обновляется `DefaultForm` (автоматически если это первая форма, или явно при `--main`)
## Детали
- FormType: Managed
- UsePurposes: PlatformApplication, MobilePlatformApplication
- AutoCommandBar с id=-1
- Реквизит "Объект" с MainAttribute=true
- BSL-модуль содержит 5 регионов: ОбработчикиСобытийФормы, ОбработчикиСобытийЭлементовФормы, ОбработчикиКомандФормы, ОбработчикиОповещений, СлужебныеПроцедурыИФункции
@@ -1,226 +0,0 @@
# epf-add-form v1.1 — Add managed form to 1C processor
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
[string]$ProcessorName,
[Parameter(Mandatory)]
[string]$FormName,
[string]$Synonym = $FormName,
[switch]$Main,
[string]$SrcDir = "src"
)
$ErrorActionPreference = "Stop"
# --- 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 '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
}
$parent = Split-Path $d -Parent
if ($parent -eq $d) { break }
$d = $parent
}
return "2.17"
}
$formatVersion = Detect-FormatVersion (Resolve-Path $SrcDir).Path
# --- Проверки ---
$rootXmlPath = Join-Path $SrcDir "$ProcessorName.xml"
if (-not (Test-Path $rootXmlPath)) {
Write-Error "Корневой файл обработки не найден: $rootXmlPath. Сначала выполните epf-init."
exit 1
}
$processorDir = Join-Path $SrcDir $ProcessorName
$formsDir = Join-Path $processorDir "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)
$encNoBom = New-Object System.Text.UTF8Encoding($false)
# --- 1. Метаданные формы (Forms/<FormName>.xml) ---
$formUuid = [guid]::NewGuid().ToString()
$formMetaXml = @"
<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject 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="$formatVersion">
<Form uuid="$formUuid">
<Properties>
<Name>$FormName</Name>
<Synonym>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>$Synonym</v8:content>
</v8:item>
</Synonym>
<Comment/>
<FormType>Managed</FormType>
<IncludeHelpInContents>false</IncludeHelpInContents>
<UsePurposes>
<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>
<v8:Value xsi:type="app:ApplicationUsePurpose">MobilePlatformApplication</v8:Value>
</UsePurposes>
<ExtendedPresentation/>
</Properties>
</Form>
</MetaDataObject>
"@
[System.IO.File]::WriteAllText($formMetaPath, $formMetaXml, $encBom)
# --- 2. Описание формы (Forms/<FormName>/Ext/Form.xml) ---
$formXmlPath = Join-Path $formExtDir "Form.xml"
$formXml = @"
<?xml version="1.0" encoding="UTF-8"?>
<Form 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" version="$formatVersion">
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
<Autofill>true</Autofill>
</AutoCommandBar>
<ChildItems/>
<Attributes>
<Attribute name="Объект" id="1">
<Type>
<v8:Type>cfg:ExternalDataProcessorObject.$ProcessorName</v8:Type>
</Type>
<MainAttribute>true</MainAttribute>
</Attribute>
</Attributes>
</Form>
"@
[System.IO.File]::WriteAllText($formXmlPath, $formXml, $encBom)
# --- 3. BSL-модуль (Forms/<FormName>/Ext/Form/Module.bsl) ---
$modulePath = Join-Path $formModuleDir "Module.bsl"
$moduleBsl = @"
#Область ОбработчикиСобытийФормы
#КонецОбласти
#Область ОбработчикиСобытийЭлементовФормы
#КонецОбласти
#Область ОбработчикиКомандФормы
#КонецОбласти
#Область ОбработчикиОповещений
#КонецОбласти
#Область СлужебныеПроцедурыИФункции
#КонецОбласти
"@
[System.IO.File]::WriteAllText($modulePath, $moduleBsl, $encBom)
# --- 4. Модификация корневого XML ---
$rootXmlFull = Resolve-Path $rootXmlPath
$xmlDoc = New-Object System.Xml.XmlDocument
$xmlDoc.PreserveWhitespace = $true
$xmlDoc.Load($rootXmlFull.Path)
$nsMgr = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable)
$nsMgr.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
$childObjects = $xmlDoc.SelectSingleNode("//md:ChildObjects", $nsMgr)
if (-not $childObjects) {
Write-Error "Не найден элемент ChildObjects в $rootXmlPath"
exit 1
}
# Добавить <Form> перед первым <Template>, или в конец
$formElem = $xmlDoc.CreateElement("Form", "http://v8.1c.ru/8.3/MDClasses")
$formElem.InnerText = $FormName
$firstTemplate = $childObjects.SelectSingleNode("md:Template", $nsMgr)
if ($firstTemplate) {
# Вставить перед Template, добавив перенос строки + табуляцию
$whitespace = $xmlDoc.CreateWhitespace("`n`t`t`t")
$childObjects.InsertBefore($whitespace, $firstTemplate) | Out-Null
$childObjects.InsertBefore($formElem, $whitespace) | Out-Null
} else {
# Добавить в конец ChildObjects
# Если ChildObjects пустой (самозакрывающийся), нужно добавить форматирование
if ($childObjects.ChildNodes.Count -eq 0) {
$childObjects.AppendChild($xmlDoc.CreateWhitespace("`n`t`t`t")) | Out-Null
$childObjects.AppendChild($formElem) | Out-Null
$childObjects.AppendChild($xmlDoc.CreateWhitespace("`n`t`t")) | Out-Null
} else {
$lastChild = $childObjects.LastChild
# Вставить перед закрывающим whitespace (если есть), или в конец
if ($lastChild.NodeType -eq [System.Xml.XmlNodeType]::Whitespace) {
$childObjects.InsertBefore($xmlDoc.CreateWhitespace("`n`t`t`t"), $lastChild) | Out-Null
$childObjects.InsertBefore($formElem, $lastChild) | Out-Null
} else {
$childObjects.AppendChild($xmlDoc.CreateWhitespace("`n`t`t`t")) | Out-Null
$childObjects.AppendChild($formElem) | Out-Null
$childObjects.AppendChild($xmlDoc.CreateWhitespace("`n`t`t")) | Out-Null
}
}
}
# Обновить DefaultForm: явно при -Main, или автоматически если это первая форма
$existingForms = $childObjects.SelectNodes("md:Form", $nsMgr)
$isFirstForm = ($existingForms.Count -eq 1)
if ($Main -or $isFirstForm) {
$defaultForm = $xmlDoc.SelectSingleNode("//md:DefaultForm", $nsMgr)
if ($defaultForm) {
$defaultForm.InnerText = "ExternalDataProcessor.$ProcessorName.Form.$FormName"
}
}
# Сохранить с BOM
$settings = New-Object System.Xml.XmlWriterSettings
$settings.Encoding = $encBom
$settings.Indent = $false # Preserve original whitespace
$stream = New-Object System.IO.FileStream($rootXmlFull.Path, [System.IO.FileMode]::Create)
$writer = [System.Xml.XmlWriter]::Create($stream, $settings)
$xmlDoc.Save($writer)
$writer.Close()
$stream.Close()
Write-Host "[OK] Создана форма: $FormName"
Write-Host " Метаданные: $formMetaPath"
Write-Host " Описание: $formXmlPath"
Write-Host " Модуль: $modulePath"
if ($Main -or $isFirstForm) {
Write-Host " DefaultForm обновлён"
}
@@ -1,272 +0,0 @@
#!/usr/bin/env python3
# add-form v1.1 — Add managed form to 1C external data processor
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import os
import re
import sys
import uuid
from lxml import etree
NSMAP = {"md": "http://v8.1c.ru/8.3/MDClasses"}
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'<MetaDataObject[^>]+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 save_xml_with_bom(tree, path):
"""Save XML tree to file with UTF-8 BOM."""
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
xml_bytes = xml_bytes.replace(b"<?xml version='1.0' encoding='UTF-8'?>", b'<?xml version="1.0" encoding="utf-8"?>')
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 write_text_with_bom(path, text):
"""Write text to file with UTF-8 BOM."""
with open(path, "w", encoding="utf-8-sig") as f:
f.write(text)
def main():
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
parser = argparse.ArgumentParser(description="Add managed form to 1C processor", allow_abbrev=False)
parser.add_argument("-ProcessorName", required=True)
parser.add_argument("-FormName", required=True)
parser.add_argument("-Synonym", default=None)
parser.add_argument("-Main", action="store_true")
parser.add_argument("-SrcDir", default="src")
args = parser.parse_args()
processor_name = args.ProcessorName
form_name = args.FormName
synonym = args.Synonym if args.Synonym is not None else form_name
is_main = args.Main
src_dir = args.SrcDir
format_version = detect_format_version(os.path.abspath(src_dir))
# --- Checks ---
root_xml_path = os.path.join(src_dir, f"{processor_name}.xml")
if not os.path.exists(root_xml_path):
print(f"Корневой файл обработки не найден: {root_xml_path}. Сначала выполните epf-init.", file=sys.stderr)
sys.exit(1)
processor_dir = os.path.join(src_dir, processor_name)
forms_dir = os.path.join(processor_dir, "Forms")
form_meta_path = os.path.join(forms_dir, f"{form_name}.xml")
if os.path.exists(form_meta_path):
print(f"Форма уже существует: {form_meta_path}", file=sys.stderr)
sys.exit(1)
# --- Create directories ---
form_dir = os.path.join(forms_dir, form_name)
form_ext_dir = os.path.join(form_dir, "Ext")
form_module_dir = os.path.join(form_ext_dir, "Form")
os.makedirs(form_module_dir, exist_ok=True)
# --- 1. Form metadata (Forms/<FormName>.xml) ---
form_uuid = str(uuid.uuid4())
form_meta_xml = (
'<?xml version="1.0" encoding="UTF-8"?>\n'
'<MetaDataObject 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"'
f' version="{format_version}">\n'
f'\t<Form uuid="{form_uuid}">\n'
'\t\t<Properties>\n'
f'\t\t\t<Name>{form_name}</Name>\n'
'\t\t\t<Synonym>\n'
'\t\t\t\t<v8:item>\n'
'\t\t\t\t\t<v8:lang>ru</v8:lang>\n'
f'\t\t\t\t\t<v8:content>{synonym}</v8:content>\n'
'\t\t\t\t</v8:item>\n'
'\t\t\t</Synonym>\n'
'\t\t\t<Comment/>\n'
'\t\t\t<FormType>Managed</FormType>\n'
'\t\t\t<IncludeHelpInContents>false</IncludeHelpInContents>\n'
'\t\t\t<UsePurposes>\n'
'\t\t\t\t<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>\n'
'\t\t\t\t<v8:Value xsi:type="app:ApplicationUsePurpose">MobilePlatformApplication</v8:Value>\n'
'\t\t\t</UsePurposes>\n'
'\t\t\t<ExtendedPresentation/>\n'
'\t\t</Properties>\n'
'\t</Form>\n'
'</MetaDataObject>'
)
write_text_with_bom(form_meta_path, form_meta_xml)
# --- 2. Form description (Forms/<FormName>/Ext/Form.xml) ---
form_xml_path = os.path.join(form_ext_dir, "Form.xml")
form_xml = (
'<?xml version="1.0" encoding="UTF-8"?>\n'
'<Form 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"'
f' version="{format_version}">\n'
'\t<AutoCommandBar name="\u0424\u043e\u0440\u043c\u0430\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c" id="-1">\n'
'\t\t<Autofill>true</Autofill>\n'
'\t</AutoCommandBar>\n'
'\t<ChildItems/>\n'
'\t<Attributes>\n'
f'\t\t<Attribute name="\u041e\u0431\u044a\u0435\u043a\u0442" id="1">\n'
'\t\t\t<Type>\n'
f'\t\t\t\t<v8:Type>cfg:ExternalDataProcessorObject.{processor_name}</v8:Type>\n'
'\t\t\t</Type>\n'
'\t\t\t<MainAttribute>true</MainAttribute>\n'
'\t\t</Attribute>\n'
'\t</Attributes>\n'
'</Form>'
)
write_text_with_bom(form_xml_path, form_xml)
# --- 3. BSL module (Forms/<FormName>/Ext/Form/Module.bsl) ---
module_path = os.path.join(form_module_dir, "Module.bsl")
module_bsl = (
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u0421\u043e\u0431\u044b\u0442\u0438\u0439\u0424\u043e\u0440\u043c\u044b\n'
'\n'
'#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438\n'
'\n'
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u0421\u043e\u0431\u044b\u0442\u0438\u0439\u042d\u043b\u0435\u043c\u0435\u043d\u0442\u043e\u0432\u0424\u043e\u0440\u043c\u044b\n'
'\n'
'#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438\n'
'\n'
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u041a\u043e\u043c\u0430\u043d\u0434\u0424\u043e\u0440\u043c\u044b\n'
'\n'
'#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438\n'
'\n'
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u041e\u043f\u043e\u0432\u0435\u0449\u0435\u043d\u0438\u0439\n'
'\n'
'#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438\n'
'\n'
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u0421\u043b\u0443\u0436\u0435\u0431\u043d\u044b\u0435\u041f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u044b\u0418\u0424\u0443\u043d\u043a\u0446\u0438\u0438\n'
'\n'
'#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438'
)
write_text_with_bom(module_path, module_bsl)
# --- 4. Modify root XML ---
root_xml_full = os.path.abspath(root_xml_path)
parser_xml = etree.XMLParser(remove_blank_text=False)
tree = etree.parse(root_xml_full, parser_xml)
root = tree.getroot()
ns = "http://v8.1c.ru/8.3/MDClasses"
child_objects = root.find(".//md:ChildObjects", NSMAP)
if child_objects is None:
print(f"Не найден элемент ChildObjects в {root_xml_path}", file=sys.stderr)
sys.exit(1)
# Add <Form> before first <Template>, or at end
form_elem = etree.Element(f"{{{ns}}}Form")
form_elem.text = form_name
first_template = child_objects.find("md:Template", NSMAP)
if first_template is not None:
# Insert before Template, adding newline + indent
idx = list(child_objects).index(first_template)
child_objects.insert(idx, form_elem)
# Set whitespace: form_elem gets same tail pattern
form_elem.tail = "\n\t\t\t"
else:
# Add to end of ChildObjects
children = list(child_objects)
if len(children) == 0 and (child_objects.text is None or child_objects.text.strip() == ""):
# Empty ChildObjects (self-closing)
child_objects.text = "\n\t\t\t"
child_objects.append(form_elem)
form_elem.tail = "\n\t\t"
else:
if len(children) > 0:
last_child = children[-1]
old_tail = last_child.tail
last_child.tail = "\n\t\t\t"
child_objects.append(form_elem)
form_elem.tail = old_tail if old_tail else "\n\t\t"
else:
child_objects.text = (child_objects.text or "") + "\n\t\t\t"
child_objects.append(form_elem)
form_elem.tail = "\n\t\t"
# Update DefaultForm: explicitly with -Main, or automatically if this is the first form
existing_forms = child_objects.findall("md:Form", NSMAP)
is_first_form = len(existing_forms) == 1
if is_main or is_first_form:
default_form = root.find(".//md:DefaultForm", NSMAP)
if default_form is not None:
default_form.text = f"ExternalDataProcessor.{processor_name}.Form.{form_name}"
# Save with BOM
save_xml_with_bom(tree, root_xml_full)
print(f"[OK] Создана форма: {form_name}")
print(f" Метаданные: {form_meta_path}")
print(f" Описание: {form_xml_path}")
print(f" Модуль: {module_path}")
if is_main or is_first_form:
print(" DefaultForm обновлён")
if __name__ == "__main__":
main()
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: epf-bsp-add-command
description: Добавить команду в дополнительную обработку БСП
description: Определить команду в БСП‑описании обработки (`СведенияОВнешнейОбработке`) — открытие формы, вызов клиентского/серверного метода, заполнение объекта и т.п. Используй когда нужно зарегистрировать команду в дополнительной обработке БСП
argument-hint: <ProcessorName> <Идентификатор> [ТипКоманды] [Представление]
allowed-tools:
- Read
+2 -2
View File
@@ -1,6 +1,6 @@
---
name: epf-bsp-init
description: Добавить функцию регистрации БСП (СведенияОВнешнейОбработке) в модуль объекта обработки
description: Сформировать функцию `СведенияОВнешнейОбработке` в модуле объекта обработки — описание для подключения через подсистему БСП «Дополнительные отчёты и обработки». Используй когда нужно сделать обработку совместимой с БСП, подключаемой через «Дополнительные отчёты и обработки»
argument-hint: <ProcessorName> <Вид>
allowed-tools:
- Read
@@ -203,6 +203,6 @@ allowed-tools:
## Дальнейшие шаги
- Добавить ещё команду: `/epf-bsp-add-command`
- Добавить форму: `/epf-add-form`
- Добавить форму: `/form-add`
- Добавить макет: `/template-add`
- Собрать EPF: `/epf-build`
+3 -3
View File
@@ -40,7 +40,7 @@ allowed-tools:
## Команда
```powershell
powershell.exe -NoProfile -File .claude/skills/epf-build/scripts/epf-build.ps1 <параметры>
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-build.ps1" <параметры>
```
### Параметры скрипта
@@ -62,8 +62,8 @@ powershell.exe -NoProfile -File .claude/skills/epf-build/scripts/epf-build.ps1 <
```powershell
# Сборка обработки (файловая база)
powershell.exe -NoProfile -File .claude/skills/epf-build/scripts/epf-build.ps1 -InfoBasePath "C:\Bases\MyDB" -SourceFile "src/МояОбработка.xml" -OutputFile "build/МояОбработка.epf"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-build.ps1" -InfoBasePath "C:\Bases\MyDB" -SourceFile "src/МояОбработка.xml" -OutputFile "build/МояОбработка.epf"
# Серверная база
powershell.exe -NoProfile -File .claude/skills/epf-build/scripts/epf-build.ps1 -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -SourceFile "src/МояОбработка.xml" -OutputFile "build/МояОбработка.epf"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-build.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -SourceFile "src/МояОбработка.xml" -OutputFile "build/МояОбработка.epf"
```
+3 -3
View File
@@ -39,7 +39,7 @@ allowed-tools:
## Команда
```powershell
powershell.exe -NoProfile -File .claude/skills/epf-dump/scripts/epf-dump.ps1 <параметры>
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-dump.ps1" <параметры>
```
### Параметры скрипта
@@ -62,8 +62,8 @@ powershell.exe -NoProfile -File .claude/skills/epf-dump/scripts/epf-dump.ps1 <п
```powershell
# Разборка обработки (файловая база)
powershell.exe -NoProfile -File .claude/skills/epf-dump/scripts/epf-dump.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "build/МояОбработка.epf" -OutputDir "src"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-dump.ps1" -InfoBasePath "C:\Bases\MyDB" -InputFile "build/МояОбработка.epf" -OutputDir "src"
# Серверная база
powershell.exe -NoProfile -File .claude/skills/epf-dump/scripts/epf-dump.ps1 -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -InputFile "build/МояОбработка.epf" -OutputDir "src"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-dump.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -InputFile "build/МояОбработка.epf" -OutputDir "src"
```
+3 -3
View File
@@ -1,6 +1,6 @@
---
name: epf-init
description: Создать пустую внешнюю обработку 1С (scaffold XML-исходников)
description: Создать пустую внешнюю обработку 1С (scaffold XML-исходников). Используй когда нужно создать новую внешнюю обработку с нуля
argument-hint: <Name> [Synonym]
allowed-tools:
- Bash
@@ -30,12 +30,12 @@ allowed-tools:
## Команда
```powershell
powershell.exe -NoProfile -File .claude/skills/epf-init/scripts/init.ps1 -Name "<Name>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"]
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/init.ps1" -Name "<Name>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"]
```
## Дальнейшие шаги
- Добавить форму: `/epf-add-form`
- Добавить форму: `/form-add`
- Добавить макет: `/template-add`
- Добавить справку: `/help-add`
- Собрать EPF: `/epf-build`
+3 -1
View File
@@ -1,4 +1,4 @@
# epf-init v1.0 — Init 1C external data processor scaffold
# epf-init v1.1 — Init 1C external data processor scaffold
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -10,6 +10,8 @@ param(
)
$ErrorActionPreference = "Stop"
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::InputEncoding = [System.Text.Encoding]::UTF8
$uuid1 = [guid]::NewGuid().ToString()
$uuid2 = [guid]::NewGuid().ToString()
+1 -1
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# epf-init v1.0 — Init 1C external data processor scaffold
# 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
+2 -2
View File
@@ -24,7 +24,7 @@ allowed-tools:
## Команда
```powershell
powershell.exe -NoProfile -File .claude/skills/epf-validate/scripts/epf-validate.ps1 -ObjectPath "src/МояОбработка"
powershell.exe -NoProfile -File .claude/skills/epf-validate/scripts/epf-validate.ps1 -ObjectPath "src/МояОбработка/МояОбработка.xml"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-validate.ps1" -ObjectPath "src/МояОбработка"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-validate.ps1" -ObjectPath "src/МояОбработка/МояОбработка.xml"
```
@@ -1,8 +1,9 @@
# epf-validate v1.1 — Validate 1C external data processor / report structure
# 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,
@@ -184,8 +185,8 @@ if ($root.NamespaceURI -ne $expectedNs) {
$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") {
Report-Warn "1. Unusual version '$version' (expected 2.17 or 2.20)"
} 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
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# epf-validate v1.1 — Validate 1C external data processor / report structure
# 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
@@ -46,7 +46,7 @@ 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", required=True)
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)
@@ -165,8 +165,8 @@ def main():
version = root.get("version", "")
if not version:
report_warn("1. Missing version attribute on MetaDataObject")
elif version not in ("2.17", "2.20"):
report_warn(f"1. Unusual version '{version}' (expected 2.17 or 2.20)")
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 = []
+3 -3
View File
@@ -42,7 +42,7 @@ allowed-tools:
Используй общий скрипт из epf-build:
```powershell
powershell.exe -NoProfile -File .claude/skills/epf-build/scripts/epf-build.ps1 <параметры>
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-build/scripts/epf-build.ps1" <параметры>
```
### Параметры скрипта
@@ -64,8 +64,8 @@ powershell.exe -NoProfile -File .claude/skills/epf-build/scripts/epf-build.ps1 <
```powershell
# Сборка отчёта (файловая база)
powershell.exe -NoProfile -File .claude/skills/epf-build/scripts/epf-build.ps1 -InfoBasePath "C:\Bases\MyDB" -SourceFile "src/МойОтчёт.xml" -OutputFile "build/МойОтчёт.erf"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-build/scripts/epf-build.ps1" -InfoBasePath "C:\Bases\MyDB" -SourceFile "src/МойОтчёт.xml" -OutputFile "build/МойОтчёт.erf"
# Серверная база
powershell.exe -NoProfile -File .claude/skills/epf-build/scripts/epf-build.ps1 -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -SourceFile "src/МойОтчёт.xml" -OutputFile "build/МойОтчёт.erf"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-build/scripts/epf-build.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -SourceFile "src/МойОтчёт.xml" -OutputFile "build/МойОтчёт.erf"
```
+3 -3
View File
@@ -41,7 +41,7 @@ allowed-tools:
Используй общий скрипт из epf-dump:
```powershell
powershell.exe -NoProfile -File .claude/skills/epf-dump/scripts/epf-dump.ps1 <параметры>
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-dump/scripts/epf-dump.ps1" <параметры>
```
### Параметры скрипта
@@ -64,8 +64,8 @@ powershell.exe -NoProfile -File .claude/skills/epf-dump/scripts/epf-dump.ps1 <п
```powershell
# Разборка отчёта (файловая база)
powershell.exe -NoProfile -File .claude/skills/epf-dump/scripts/epf-dump.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "build/МойОтчёт.erf" -OutputDir "src"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-dump/scripts/epf-dump.ps1" -InfoBasePath "C:\Bases\MyDB" -InputFile "build/МойОтчёт.erf" -OutputDir "src"
# Серверная база
powershell.exe -NoProfile -File .claude/skills/epf-dump/scripts/epf-dump.ps1 -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -InputFile "build/МойОтчёт.erf" -OutputDir "src"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-dump/scripts/epf-dump.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -InputFile "build/МойОтчёт.erf" -OutputDir "src"
```
+2 -2
View File
@@ -1,6 +1,6 @@
---
name: erf-init
description: Создать пустой внешний отчёт 1С (scaffold XML-исходников)
description: Создать пустой внешний отчёт 1С (scaffold XML-исходников). Используй когда нужно создать новый внешний отчёт с нуля
argument-hint: <Name> [Synonym] [--with-skd]
allowed-tools:
- Bash
@@ -31,7 +31,7 @@ allowed-tools:
## Команда
```powershell
powershell.exe -NoProfile -File .claude/skills/erf-init/scripts/init.ps1 -Name "<Name>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"] [-WithSKD]
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/init.ps1" -Name "<Name>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"] [-WithSKD]
```
## Дальнейшие шаги
+3 -1
View File
@@ -1,4 +1,4 @@
# erf-init v1.0 — Init 1C external report scaffold
# erf-init v1.1 — Init 1C external report scaffold
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -12,6 +12,8 @@ param(
)
$ErrorActionPreference = "Stop"
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::InputEncoding = [System.Text.Encoding]::UTF8
$uuid1 = [guid]::NewGuid().ToString()
$uuid2 = [guid]::NewGuid().ToString()
+1 -1
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# erf-init v1.0 — Init 1C external report scaffold
# 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
+2 -2
View File
@@ -26,7 +26,7 @@ allowed-tools:
## Команда
```powershell
powershell.exe -NoProfile -File .claude/skills/epf-validate/scripts/epf-validate.ps1 -ObjectPath "src/МойОтчёт"
powershell.exe -NoProfile -File .claude/skills/epf-validate/scripts/epf-validate.ps1 -ObjectPath "src/МойОтчёт/МойОтчёт.xml"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-validate/scripts/epf-validate.ps1" -ObjectPath "src/МойОтчёт"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-validate/scripts/epf-validate.ps1" -ObjectPath "src/МойОтчёт/МойОтчёт.xml"
```
+2 -2
View File
@@ -1,6 +1,6 @@
---
name: form-add
description: Добавить управляемую форму к объекту конфигурации 1С
description: Добавить пустую управляемую форму к объекту 1С. Используй когда нужно создать у объекта новую форму
argument-hint: <ObjectPath> <FormName> [Purpose] [--set-default]
allowed-tools:
- Bash
@@ -32,7 +32,7 @@ allowed-tools:
## Команда
```powershell
powershell.exe -NoProfile -File .claude/skills/form-add/scripts/form-add.ps1 -ObjectPath "<ObjectPath>" -FormName "<FormName>" [-Purpose "<Purpose>"] [-Synonym "<Synonym>"] [-SetDefault]
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-add.ps1" -ObjectPath "<ObjectPath>" -FormName "<FormName>" [-Purpose "<Purpose>"] [-Synonym "<Synonym>"] [-SetDefault]
```
## Purpose — назначение формы
+10 -17
View File
@@ -1,4 +1,4 @@
# form-add v1.3 — Add managed form to 1C config object
# form-add v1.5 — Add managed form to 1C config object
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -15,6 +15,8 @@ param(
)
$ErrorActionPreference = "Stop"
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::InputEncoding = [System.Text.Encoding]::UTF8
# --- Detect XML format version ---
@@ -219,9 +221,6 @@ if ($Purpose -eq "List" -or $Purpose -eq "Choice") {
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
<Autofill>true</Autofill>
</AutoCommandBar>
<Events>
<Event name="OnCreateAtServer">ПриСозданииНаСервере</Event>
</Events>
<ChildItems/>
<Attributes>
<Attribute name="Список" id="1">
@@ -247,9 +246,6 @@ if ($Purpose -eq "List" -or $Purpose -eq "Choice") {
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
<Autofill>true</Autofill>
</AutoCommandBar>
<Events>
<Event name="OnCreateAtServer">ПриСозданииНаСервере</Event>
</Events>
<ChildItems/>
<Attributes>
<Attribute name="$mainAttrName" id="1">
@@ -285,23 +281,25 @@ if ($Purpose -eq "List" -or $Purpose -eq "Choice") {
$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`t<SavedData>true</SavedData>"
}
$formXml = @"
<?xml version="1.0" encoding="UTF-8"?>
<Form $formNsDecl version="$($script:formatVersion)">
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
<Autofill>true</Autofill>
</AutoCommandBar>
<Events>
<Event name="OnCreateAtServer">ПриСозданииНаСервере</Event>
</Events>
<ChildItems/>
<Attributes>
<Attribute name="$mainAttrName" id="1">
<Type>
<v8:Type>cfg:$mainAttrType</v8:Type>
</Type>
<MainAttribute>true</MainAttribute>
<SavedData>true</SavedData>
<MainAttribute>true</MainAttribute>$savedDataLine
</Attribute>
</Attributes>
</Form>
@@ -321,11 +319,6 @@ $modulePath = Join-Path $formModuleDir "Module.bsl"
$moduleBsl = @"
#Область ОбработчикиСобытийФормы
&НаСервере
Процедура ПриСозданииНаСервере(Отказ, СтандартнаяОбработка)
КонецПроцедуры
#КонецОбласти
#Область ОбработчикиСобытийЭлементовФормы
+7 -16
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# form-add v1.3 — Add managed form to 1C config object
# form-add v1.5 — Add managed form to 1C config object
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -248,9 +248,6 @@ def main():
'\t<AutoCommandBar name="\u0424\u043e\u0440\u043c\u0430\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c" id="-1">\n'
'\t\t<Autofill>true</Autofill>\n'
'\t</AutoCommandBar>\n'
'\t<Events>\n'
'\t\t<Event name="OnCreateAtServer">\u041f\u0440\u0438\u0421\u043e\u0437\u0434\u0430\u043d\u0438\u0438\u041d\u0430\u0421\u0435\u0440\u0432\u0435\u0440\u0435</Event>\n'
'\t</Events>\n'
'\t<ChildItems/>\n'
'\t<Attributes>\n'
'\t\t<Attribute name="\u0421\u043f\u0438\u0441\u043e\u043a" id="1">\n'
@@ -277,9 +274,6 @@ def main():
'\t<AutoCommandBar name="\u0424\u043e\u0440\u043c\u0430\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c" id="-1">\n'
'\t\t<Autofill>true</Autofill>\n'
'\t</AutoCommandBar>\n'
'\t<Events>\n'
'\t\t<Event name="OnCreateAtServer">\u041f\u0440\u0438\u0421\u043e\u0437\u0434\u0430\u043d\u0438\u0438\u041d\u0430\u0421\u0435\u0440\u0432\u0435\u0440\u0435</Event>\n'
'\t</Events>\n'
'\t<ChildItems/>\n'
'\t<Attributes>\n'
f'\t\t<Attribute name="{main_attr_name}" id="1">\n'
@@ -315,15 +309,17 @@ def main():
main_attr_type = f"{attr_type_map[object_type]}.{object_name}"
# SavedData: standard for Catalog/Document/etc, but not for processor-like (DataProcessor/Report/External*)
saved_data_line = ''
if object_type not in processor_like_types:
saved_data_line = '\t\t\t<SavedData>true</SavedData>\n'
form_xml = (
f'<?xml version="1.0" encoding="UTF-8"?>\n'
f'<Form {form_ns_decl} version="{format_version}">\n'
'\t<AutoCommandBar name="\u0424\u043e\u0440\u043c\u0430\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c" id="-1">\n'
'\t\t<Autofill>true</Autofill>\n'
'\t</AutoCommandBar>\n'
'\t<Events>\n'
'\t\t<Event name="OnCreateAtServer">\u041f\u0440\u0438\u0421\u043e\u0437\u0434\u0430\u043d\u0438\u0438\u041d\u0430\u0421\u0435\u0440\u0432\u0435\u0440\u0435</Event>\n'
'\t</Events>\n'
'\t<ChildItems/>\n'
'\t<Attributes>\n'
f'\t\t<Attribute name="{main_attr_name}" id="1">\n'
@@ -331,7 +327,7 @@ def main():
f'\t\t\t\t<v8:Type>cfg:{main_attr_type}</v8:Type>\n'
'\t\t\t</Type>\n'
'\t\t\t<MainAttribute>true</MainAttribute>\n'
'\t\t\t<SavedData>true</SavedData>\n'
f'{saved_data_line}'
'\t\t</Attribute>\n'
'\t</Attributes>\n'
'</Form>'
@@ -349,11 +345,6 @@ def main():
module_bsl = (
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u0421\u043e\u0431\u044b\u0442\u0438\u0439\u0424\u043e\u0440\u043c\u044b\n'
'\n'
'&\u041d\u0430\u0421\u0435\u0440\u0432\u0435\u0440\u0435\n'
'\u041f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u0430 \u041f\u0440\u0438\u0421\u043e\u0437\u0434\u0430\u043d\u0438\u0438\u041d\u0430\u0421\u0435\u0440\u0432\u0435\u0440\u0435(\u041e\u0442\u043a\u0430\u0437, \u0421\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u0430\u044f\u041e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430)\n'
'\n'
'\u041a\u043e\u043d\u0435\u0446\u041f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u044b\n'
'\n'
'#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438\n'
'\n'
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u0421\u043e\u0431\u044b\u0442\u0438\u0439\u042d\u043b\u0435\u043c\u0435\u043d\u0442\u043e\u0432\u0424\u043e\u0440\u043c\u044b\n'
+95 -8
View File
@@ -29,10 +29,10 @@ allowed-tools:
```powershell
# Режим JSON DSL
powershell.exe -NoProfile -File .claude/skills/form-compile/scripts/form-compile.ps1 -JsonPath "<json>" -OutputPath "<Form.xml>"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-compile.ps1" -JsonPath "<json>" -OutputPath "<Form.xml>"
# Режим from-object (объект и purpose выводятся из OutputPath; Document и Catalog)
powershell.exe -NoProfile -File .claude/skills/form-compile/scripts/form-compile.ps1 -FromObject -OutputPath "<.../TypePlural/ObjectName/Forms/FormName/Ext/Form.xml>"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-compile.ps1" -FromObject -OutputPath "<.../TypePlural/ObjectName/Forms/FormName/Ext/Form.xml>"
```
## JSON DSL — справка
@@ -62,8 +62,10 @@ powershell.exe -NoProfile -File .claude/skills/form-compile/scripts/form-compile
| DSL ключ | XML элемент | Значение ключа |
|--------------|-------------------|---------------------------------------------------|
| `"group"` | UsualGroup | `"horizontal"` / `"vertical"` / `"alwaysHorizontal"` / `"alwaysVertical"` / `"collapsible"` |
| `"columnGroup"` | ColumnGroup | `"horizontal"` / `"vertical"` / `"inCell"` — только внутри `columns` таблицы |
| `"input"` | InputField | имя элемента |
| `"check"` | CheckBoxField | имя |
| `"radio"` | RadioButtonField | имя |
| `"label"` | LabelDecoration | имя (текст задаётся через `title`) |
| `"labelField"` | LabelField | имя |
| `"table"` | Table | имя |
@@ -74,6 +76,7 @@ powershell.exe -NoProfile -File .claude/skills/form-compile/scripts/form-compile
| `"picField"` | PictureField | имя |
| `"calendar"` | CalendarField | имя |
| `"cmdBar"` | CommandBar | имя |
| `"autoCmdBar"` | AutoCommandBar формы | имя — наполняет главную АКП формы (id=-1), не попадает в `<ChildItems>` |
| `"popup"` | Popup | имя |
### Общие свойства (все типы элементов)
@@ -96,7 +99,7 @@ powershell.exe -NoProfile -File .claude/skills/form-compile/scripts/form-compile
**input / picField**: `OnChange`, `StartChoice`, `ChoiceProcessing`, `AutoComplete`, `TextEditEnd`, `Clearing`, `Creating`, `EditTextChange`
**check**: `OnChange`
**check / radio**: `OnChange`
**table**: `OnStartEdit`, `OnEditEnd`, `OnChange`, `Selection`, `ValueChoice`, `BeforeAddRow`, `BeforeDeleteRow`, `AfterDeleteRow`, `BeforeRowChange`, `BeforeEditEnd`, `OnActivateRow`, `OnActivateCell`, `Drag`, `DragStart`, `DragCheck`, `DragEnd`
@@ -124,7 +127,8 @@ powershell.exe -NoProfile -File .claude/skills/form-compile/scripts/form-compile
| `skipOnInput: true` | Пропускать при обходе Tab | |
| `inputHint` | Подсказка в пустом поле | `"Введите наименование..."` |
| `width` / `height` | Размер | числа |
| `autoMaxWidth: false` | Отключить авто-ширину | для фиксированных полей |
| `autoMaxWidth: false` | Снять авто-ограничение ширины (поле растянется) | |
| `maxWidth` / `maxHeight` | Жёсткое ограничение размера | числа; обычно вместе с `autoMaxWidth: false` |
| `horizontalStretch: true` | Растягивать по ширине | |
### Чекбокс (check)
@@ -134,6 +138,37 @@ powershell.exe -NoProfile -File .claude/skills/form-compile/scripts/form-compile
| `path` | DataPath |
| `titleLocation` | Размещение заголовка |
### Поле переключателя (radio)
Радиокнопки или тумблер для выбора одного значения из списка.
| Ключ | Описание | Пример |
|------|----------|--------|
| `path` | DataPath — привязка к реквизиту | `"СпособКурса"` |
| `radioButtonType` | Вид переключателя | `"Auto"` (по умолчанию), `"RadioButtons"`, `"Tumbler"` |
| `columnsCount` | Число колонок раскладки | `1`, `2`, ... |
| `titleLocation` | Размещение заголовка | по умолчанию `"none"` |
| `choiceList` | Список вариантов: массив `{value, presentation}` | см. ниже |
`choiceList[*]`:
| Ключ | Описание |
|------|----------|
| `value` | Значение варианта. Строка/число/булево; для перечисления — `"Enum.ИмяТипа.EnumValue.ИмяЗначения"` |
| `presentation` | Текст рядом с переключателем. Строка (русский) либо объект `{ru, en, ...}` для мультиязычности |
```json
{
"radio": "СпособКурса",
"path": "Объект.СпособУстановкиКурса",
"radioButtonType": "Auto",
"choiceList": [
{ "value": "Enum.СпособыКурса.EnumValue.Авто", "presentation": { "ru": "Автоматически", "en": "Automatic" } },
{ "value": "Enum.СпособыКурса.EnumValue.Ручной", "presentation": "вручную" }
]
}
```
### Надпись-декорация (label)
| Ключ | Описание |
@@ -149,7 +184,8 @@ powershell.exe -NoProfile -File .claude/skills/form-compile/scripts/form-compile
| Ключ | Описание |
|------|----------|
| `showTitle: true` | Показывать заголовок группы |
| `united: false` | Не объединять рамку |
| `united: false` | Левый край полей ввода выравнивается только в пределах этой группы (по умолчанию `true` — сквозное выравнивание по самому длинному заголовку, в т.ч. с соседними группами) |
| `collapsed: true` | Только для `"group": "collapsible"` — группа создаётся свёрнутой |
| `representation` | `"none"`, `"normal"`, `"weak"`, `"strong"` |
| `children: [...]` | Вложенные элементы |
@@ -175,6 +211,35 @@ powershell.exe -NoProfile -File .claude/skills/form-compile/scripts/form-compile
| `rowPictureDataPath` | Путь к картинке строки (напр. `"Список.DefaultPicture"`) |
| `tableAutofill: false` | Управление Autofill внутреннего AutoCommandBar |
Колонки можно группировать через `columnGroup` (см. ниже).
### Группа колонок (columnGroup)
Используется только внутри `columns` таблицы. Значение ключа задаёт ориентацию: `"horizontal"`, `"vertical"`, `"inCell"` (склеивает колонки в одну ячейку шапки). Допускается вложение `columnGroup` в `columnGroup`.
| Ключ | Описание |
|------|----------|
| `name` | Имя элемента (рекомендуется задавать явно) |
| `title` | Заголовок группы |
| `showTitle: false` | Скрыть заголовок |
| `showInHeader: true/false` | Показывать ли группу в шапке таблицы |
| `width` | Ширина |
| `horizontalStretch: false` | Растягивание |
| `children: [...]` | Колонки внутри группы (`input`, `labelField`, `picField`, вложенный `columnGroup` …) |
```json
{ "table": "Список", "path": "Список", "columns": [
{ "columnGroup": "horizontal", "name": "ГруппаДата", "title": "Срок", "children": [
{ "input": "СрокИсполнения", "path": "Список.СрокИсполнения" },
{ "labelField": "Просрочено", "path": "Список.Просрочено" }
]},
{ "columnGroup": "inCell", "name": "ГруппаИсполнитель", "showInHeader": true, "children": [
{ "input": "Исполнитель", "path": "Список.Исполнитель" }
]},
{ "input": "Комментарий", "path": "Список.Комментарий" }
]}
```
### Страницы (pages + page)
| Ключ (pages) | Описание |
@@ -195,18 +260,39 @@ powershell.exe -NoProfile -File .claude/skills/form-compile/scripts/form-compile
| `command` | Имя команды формы → `Form.Command.Имя` |
| `stdCommand` | Стандартная команда: `"Close"``Form.StandardCommand.Close`; с точкой: `"Товары.Add"``Form.Item.Товары.StandardCommand.Add` |
| `defaultButton: true` | Кнопка по умолчанию |
| `type` | `"usual"`, `"hyperlink"`, `"commandBar"` |
| `type` | `"usual"`, `"hyperlink"`. По умолчанию `"usual"`. Конкретный XML-вид (UsualButton/Hyperlink/CommandBarButton/CommandBarHyperlink) подставляется автоматически по контексту |
| `picture` | Картинка кнопки |
| `representation` | `"Auto"`, `"Text"`, `"Picture"`, `"PictureAndText"` |
| `locationInCommandBar` | `"Auto"`, `"InCommandBar"`, `"InAdditionalSubmenu"` |
### Командная панель (cmdBar)
Дополнительная пользовательская панель команд, размещается как обычный элемент в layout формы.
| Ключ | Описание |
|------|----------|
| `autofill: true` | Автозаполнение стандартными командами |
| `children: [...]` | Кнопки панели |
### Главная автокомандная панель формы (autoCmdBar)
Наполняет встроенную AutoCommandBar формы (id=-1) кастомными кнопками. Указывать только если нужно добавить свои кнопки на главную панель или явно управлять автозаполнением.
| Ключ | Описание |
|------|----------|
| `autofill: true/false` | Автозаполнение стандартными командами |
| `horizontalAlign` | `"Left"` / `"Center"` / `"Right"` |
| `children: [...]` | Кнопки/popup |
```json
{ "autoCmdBar": "ФормаКоманднаяПанель", "autofill": true, "children": [
{ "button": "ИзменитьВыделенные", "command": "ИзменитьВыделенные",
"locationInCommandBar": "InAdditionalSubmenu" }
]}
```
Кнопки основных действий формы и подменю размещают здесь, а не в отдельной группе на форме. Отдельной кнопкой в layout — только если она логически привязана к конкретному полю или группе.
### Выпадающее меню (popup)
| Ключ | Описание |
@@ -239,6 +325,7 @@ powershell.exe -NoProfile -File .claude/skills/form-compile/scripts/form-compile
```
- `savedData: true` — сохраняемые данные
- `main: true` — главный реквизит формы (например, основной `*Object.*`, `DynamicList`, `*RecordSet.*`)
### Команды (commands)
@@ -348,9 +435,9 @@ powershell.exe -NoProfile -File .claude/skills/form-compile/scripts/form-compile
{ "check": "ПерваяСтрокаЗаголовок", "path": "ПерваяСтрокаЗаголовок" }
]},
{ "input": "Результат", "path": "Результат", "multiLine": true, "height": 8, "readOnly": true, "title": "Лог" },
{ "group": "horizontal", "name": "ГруппаКнопок", "children": [
{ "autoCmdBar": "ФормаКоманднаяПанель", "children": [
{ "button": "Загрузить", "command": "Загрузить", "defaultButton": true },
{ "button": "Закрыть", "stdCommand": "Close" }
{ "button": "Закрыть", "stdCommand": "Close" }
]}
],
"attributes": [
@@ -1,4 +1,4 @@
# form-compile v1.6 — Compile 1C managed form from JSON or object metadata
# form-compile v1.20 — Compile 1C managed form from JSON or object metadata
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[string]$JsonPath,
@@ -668,6 +668,7 @@ function Generate-DocumentListDSL($meta, [hashtable]$p) {
$tableEl = [ordered]@{
table = "Список"; path = "Список"
rowPictureDataPath = "Список.DefaultPicture"
commandBarLocation = "None"
tableAutofill = $false
columns = $columns
@@ -1003,6 +1004,7 @@ function Generate-InformationRegisterListDSL($meta, [hashtable]$p) {
$tableEl = [ordered]@{
table = "Список"; path = "Список"
rowPictureDataPath = "Список.DefaultPicture"
commandBarLocation = "None"
tableAutofill = $false
columns = $columns
@@ -1060,6 +1062,7 @@ function Generate-AccumulationRegisterListDSL($meta, [hashtable]$p) {
$tableEl = [ordered]@{
table = "Список"; path = "Список"
rowPictureDataPath = "Список.DefaultPicture"
commandBarLocation = "None"
tableAutofill = $false
columns = $columns
@@ -1554,7 +1557,7 @@ $script:formTypeSynonyms["определяемыйтип"] = "Define
# Known invalid types (runtime/UI types that don't exist in XDTO schema)
$script:knownInvalidTypes = @{
"FormDataStructure" = "Runtime type. Use cfg:*Object.XXX (e.g. CatalogObject.XXX)"
"FormDataStructure" = "Runtime type. Use object type without cfg: prefix (e.g. CatalogObject.Контрагенты, DocumentObject.Приход)"
"FormDataCollection" = "Runtime type. Use ValueTable"
"FormDataTree" = "Runtime type. Use ValueTree"
"FormDataTreeItem" = "Runtime type, not valid in XML"
@@ -1569,6 +1572,8 @@ $script:knownInvalidTypes = @{
function Resolve-TypeStr {
param([string]$typeStr)
if (-not $typeStr) { return $typeStr }
# Lenient: strip leading cfg: prefix if user passed it (canonical form is without prefix)
if ($typeStr -match '^cfg:(.+)$') { $typeStr = $Matches[1] }
if ($typeStr -match '^([^(]+)\((.+)\)$') {
$base = $Matches[1].Trim(); $params = $Matches[2]
$r = $script:formTypeSynonyms[$base.ToLower()]
@@ -1769,6 +1774,7 @@ function Get-ElementName {
$script:knownEvents = @{
"input" = @("OnChange","StartChoice","ChoiceProcessing","AutoComplete","TextEditEnd","Clearing","Creating","EditTextChange")
"check" = @("OnChange")
"radio" = @("OnChange")
"label" = @("Click","URLProcessing")
"labelField"= @("OnChange","StartChoice","ChoiceProcessing","Click","URLProcessing","Clearing")
"table" = @("Selection","BeforeAddRow","AfterDeleteRow","BeforeDeleteRow","OnActivateRow","OnEditEnd","OnStartEdit","BeforeRowChange","BeforeEditEnd","ValueChoice","OnActivateCell","OnActivateField","Drag","DragStart","DragCheck","DragEnd","OnGetDataAtServer","BeforeLoadUserSettingsAtServer","OnUpdateUserSettingSetAtServer","OnChange")
@@ -1819,13 +1825,60 @@ function Emit-Companion {
}
function Emit-Element {
param($el, [string]$indent)
param($el, [string]$indent, [bool]$inCmdBar = $false)
# Silent synonyms: model often writes XML name or Russian (ПолеПереключателя/RadioButtonField → radio).
# Maps any synonym to canonical short DSL key.
$synonyms = @{
"commandBar" = "cmdBar"
"autoCommandBar" = "autoCmdBar"
"КоманднаяПанель" = "cmdBar"
"InputField" = "input"
"ПолеВвода" = "input"
"CheckBoxField" = "check"
"ПолеФлажка" = "check"
"RadioButtonField" = "radio"
"ПолеПереключателя" = "radio"
"radioButton" = "radio"
"PictureField" = "picField"
"ПолеКартинки" = "picField"
"LabelField" = "labelField"
"ПолеНадписи" = "labelField"
"CalendarField" = "calendar"
"ПолеКалендаря" = "calendar"
"LabelDecoration" = "label"
"Надпись" = "label"
"PictureDecoration" = "picture"
"Картинка" = "picture"
"UsualGroup" = "group"
"Группа" = "group"
"ОбычнаяГруппа" = "group"
"ColumnGroup" = "columnGroup"
"ГруппаКолонок" = "columnGroup"
"Pages" = "pages"
"ГруппаСтраниц" = "pages"
"Page" = "page"
"Страница" = "page"
"Table" = "table"
"Таблица" = "table"
"Button" = "button"
"Кнопка" = "button"
"Popup" = "popup"
"ВсплывающееМеню" = "popup"
}
foreach ($pair in $synonyms.GetEnumerator()) {
if ($null -ne $el.PSObject.Properties[$pair.Key] -and $null -eq $el.PSObject.Properties[$pair.Value]) {
$val = $el.($pair.Key)
$el.PSObject.Properties.Remove($pair.Key) | Out-Null
$el | Add-Member -NotePropertyName $pair.Value -NotePropertyValue $val -Force
}
}
# Determine element type from key
$typeKey = $null
$xmlTag = $null
foreach ($key in @("group","input","check","label","labelField","table","pages","page","button","picture","picField","calendar","cmdBar","popup")) {
foreach ($key in @("columnGroup","group","input","check","radio","label","labelField","table","pages","page","button","picture","picField","calendar","cmdBar","popup")) {
if ($el.$key -ne $null) {
$typeKey = $key
break
@@ -1840,8 +1893,12 @@ function Emit-Element {
# Validate known keys — warn about typos and unknown properties
$knownKeys = @{
# type keys
"group"=1;"input"=1;"check"=1;"label"=1;"labelField"=1;"table"=1;"pages"=1;"page"=1
"group"=1;"columnGroup"=1;"input"=1;"check"=1;"radio"=1;"label"=1;"labelField"=1;"table"=1;"pages"=1;"page"=1
"button"=1;"picture"=1;"picField"=1;"calendar"=1;"cmdBar"=1;"popup"=1
# columnGroup-specific
"showInHeader"=1
# radio-specific
"radioButtonType"=1;"choiceList"=1;"columnsCount"=1
# naming & binding
"name"=1;"path"=1;"title"=1
# visibility & state
@@ -1851,13 +1908,14 @@ function Emit-Element {
# layout
"titleLocation"=1;"representation"=1;"width"=1;"height"=1
"horizontalStretch"=1;"verticalStretch"=1;"autoMaxWidth"=1;"autoMaxHeight"=1
"maxWidth"=1;"maxHeight"=1
# input-specific
"multiLine"=1;"passwordMode"=1;"choiceButton"=1;"clearButton"=1
"spinButton"=1;"dropListButton"=1;"markIncomplete"=1;"skipOnInput"=1;"inputHint"=1
# label/hyperlink
"hyperlink"=1
# group-specific
"showTitle"=1;"united"=1
"showTitle"=1;"united"=1;"collapsed"=1
# hierarchy
"children"=1;"columns"=1
# table-specific
@@ -1885,14 +1943,16 @@ function Emit-Element {
switch ($typeKey) {
"group" { Emit-Group -el $el -name $name -id $id -indent $indent }
"columnGroup" { Emit-ColumnGroup -el $el -name $name -id $id -indent $indent }
"input" { Emit-Input -el $el -name $name -id $id -indent $indent }
"check" { Emit-Check -el $el -name $name -id $id -indent $indent }
"radio" { Emit-Radio -el $el -name $name -id $id -indent $indent }
"label" { Emit-Label -el $el -name $name -id $id -indent $indent }
"labelField" { Emit-LabelField -el $el -name $name -id $id -indent $indent }
"table" { Emit-Table -el $el -name $name -id $id -indent $indent }
"pages" { Emit-Pages -el $el -name $name -id $id -indent $indent }
"page" { Emit-Page -el $el -name $name -id $id -indent $indent }
"button" { Emit-Button -el $el -name $name -id $id -indent $indent }
"button" { Emit-Button -el $el -name $name -id $id -indent $indent -inCmdBar $inCmdBar }
"picture" { Emit-PictureDecoration -el $el -name $name -id $id -indent $indent }
"picField" { Emit-PictureField -el $el -name $name -id $id -indent $indent }
"calendar" { Emit-Calendar -el $el -name $name -id $id -indent $indent }
@@ -1913,10 +1973,34 @@ function Emit-CommonFlags {
if ($el.readOnly -eq $true) { X "$indent<ReadOnly>true</ReadOnly>" }
}
function Title-FromName {
param([string]$name)
if (-not $name) { return '' }
$s = [regex]::Replace($name, '([А-ЯA-Z])([А-ЯA-Z][а-яa-z])', '$1 $2')
$s = [regex]::Replace($s, '([а-яa-z0-9])([А-ЯA-Z])', '$1 $2')
$parts = $s -split ' '
if ($parts.Count -eq 0) { return $s }
$out = New-Object System.Collections.ArrayList
[void]$out.Add($parts[0])
for ($i = 1; $i -lt $parts.Count; $i++) {
$p = $parts[$i]
if ($p.Length -gt 1 -and $p -ceq $p.ToUpper()) {
[void]$out.Add($p)
} else {
[void]$out.Add($p.ToLower())
}
}
return ($out -join ' ')
}
function Emit-Title {
param($el, [string]$name, [string]$indent)
if ($el.title) {
Emit-MLText -tag "Title" -text "$($el.title)" -indent $indent
param($el, [string]$name, [string]$indent, [switch]$auto)
$title = $el.title
if (-not $title -and $auto -and $name) {
$title = Title-FromName -name $name
}
if ($title) {
Emit-MLText -tag "Title" -text "$title" -indent $indent
}
}
@@ -1943,6 +2027,7 @@ function Emit-Group {
if ($groupVal -eq "collapsible") {
X "$inner<Group>Vertical</Group>"
X "$inner<Behavior>Collapsible</Behavior>"
if ($el.collapsed -eq $true) { X "$inner<Collapsed>true</Collapsed>" }
}
# Representation
@@ -1980,6 +2065,48 @@ function Emit-Group {
X "$indent</UsualGroup>"
}
function Emit-ColumnGroup {
param($el, [string]$name, [int]$id, [string]$indent)
X "$indent<ColumnGroup name=`"$name`" id=`"$id`">"
$inner = "$indent`t"
Emit-Title -el $el -name $name -indent $inner
# Group orientation (horizontal / vertical / inCell — последнее только здесь)
$groupVal = "$($el.columnGroup)"
$orientation = switch ($groupVal) {
"horizontal" { "Horizontal" }
"vertical" { "Vertical" }
"inCell" { "InCell" }
default { $null }
}
if ($orientation) { X "$inner<Group>$orientation</Group>" }
if ($el.showTitle -eq $false) { X "$inner<ShowTitle>false</ShowTitle>" }
if ($null -ne $el.showInHeader) {
$shVal = if ($el.showInHeader) { "true" } else { "false" }
X "$inner<ShowInHeader>$shVal</ShowInHeader>"
}
if ($el.width) { X "$inner<Width>$($el.width)</Width>" }
Emit-CommonFlags -el $el -indent $inner
# Companion: ExtendedTooltip
Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner
# Children
if ($el.children -and $el.children.Count -gt 0) {
X "$inner<ChildItems>"
foreach ($child in $el.children) {
Emit-Element -el $child -indent "$inner`t"
}
X "$inner</ChildItems>"
}
X "$indent</ColumnGroup>"
}
function Emit-Input {
param($el, [string]$name, [int]$id, [string]$indent)
@@ -1988,7 +2115,7 @@ function Emit-Input {
if ($el.path) { X "$inner<DataPath>$($el.path)</DataPath>" }
Emit-Title -el $el -name $name -indent $inner
Emit-Title -el $el -name $name -indent $inner -auto:(-not $el.path)
Emit-CommonFlags -el $el -indent $inner
if ($el.titleLocation) {
@@ -2011,8 +2138,15 @@ function Emit-Input {
if ($el.dropListButton -eq $true) { X "$inner<DropListButton>true</DropListButton>" }
if ($el.markIncomplete -eq $true) { X "$inner<AutoMarkIncomplete>true</AutoMarkIncomplete>" }
if ($el.skipOnInput -eq $true) { X "$inner<SkipOnInput>true</SkipOnInput>" }
if ($el.autoMaxWidth -eq $false) { X "$inner<AutoMaxWidth>false</AutoMaxWidth>" }
$hasAmw = $el.PSObject.Properties.Name -contains 'autoMaxWidth'
if ($hasAmw) {
if ($el.autoMaxWidth -eq $false) { X "$inner<AutoMaxWidth>false</AutoMaxWidth>" }
} elseif ($el.multiLine -eq $true) {
X "$inner<AutoMaxWidth>false</AutoMaxWidth>"
}
if ($null -ne $el.maxWidth) { X "$inner<MaxWidth>$($el.maxWidth)</MaxWidth>" }
if ($el.autoMaxHeight -eq $false) { X "$inner<AutoMaxHeight>false</AutoMaxHeight>" }
if ($null -ne $el.maxHeight) { X "$inner<MaxHeight>$($el.maxHeight)</MaxHeight>" }
if ($el.width) { X "$inner<Width>$($el.width)</Width>" }
if ($el.height) { X "$inner<Height>$($el.height)</Height>" }
if ($el.horizontalStretch -eq $true) { X "$inner<HorizontalStretch>true</HorizontalStretch>" }
@@ -2039,12 +2173,11 @@ function Emit-Check {
if ($el.path) { X "$inner<DataPath>$($el.path)</DataPath>" }
Emit-Title -el $el -name $name -indent $inner
Emit-Title -el $el -name $name -indent $inner -auto:(-not $el.path)
Emit-CommonFlags -el $el -indent $inner
if ($el.titleLocation) {
X "$inner<TitleLocation>$($el.titleLocation)</TitleLocation>"
}
$tl = if ($el.titleLocation) { "$($el.titleLocation)" } else { "Right" }
X "$inner<TitleLocation>$tl</TitleLocation>"
# Companions
Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner
@@ -2055,18 +2188,237 @@ function Emit-Check {
X "$indent</CheckBoxField>"
}
# Maps Russian/English root of a typed reference path to canonical English root.
# Used to normalize ChoiceList values like "Перечисление.X.Y" → "Enum.X.EnumValue.Y".
$script:refRootSynonyms = @{
"Перечисление" = "Enum"
"Справочник" = "Catalog"
"Документ" = "Document"
"ПланСчетов" = "ChartOfAccounts"
"ПланВидовХарактеристик" = "ChartOfCharacteristicTypes"
"ПланВидовРасчета" = "ChartOfCalculationTypes"
"ПланВидовРасчёта" = "ChartOfCalculationTypes"
"ПланОбмена" = "ExchangePlan"
"БизнесПроцесс" = "BusinessProcess"
"Задача" = "Task"
"РегистрСведений" = "InformationRegister"
"РегистрНакопления" = "AccumulationRegister"
"РегистрБухгалтерии" = "AccountingRegister"
"РегистрРасчета" = "CalculationRegister"
"РегистрРасчёта" = "CalculationRegister"
}
$script:enumValueSynonyms = @("EnumValue","ЗначениеПеречисления")
# Normalize a choiceList item value: returns @{ XsiType = "..."; Text = "..." }
function Normalize-ChoiceValue {
param($value)
# Booleans
if ($value -is [bool]) {
return @{ XsiType = "xs:boolean"; Text = if ($value) { "true" } else { "false" } }
}
# Numbers (int / decimal / double)
if ($value -is [int] -or $value -is [long] -or $value -is [double] -or $value -is [decimal]) {
return @{ XsiType = "xs:decimal"; Text = "$value" }
}
$s = "$value"
if ([string]::IsNullOrEmpty($s)) {
return @{ XsiType = "xs:string"; Text = "" }
}
# Try to detect typed reference path: "<Root>.<Type>[.<Member>.<Value>]"
$parts = $s -split '\.'
if ($parts.Count -ge 2) {
$root = $parts[0]
$canonRoot = $null
if ($script:refRootSynonyms.ContainsKey($root)) { $canonRoot = $script:refRootSynonyms[$root] }
elseif ($script:refRootSynonyms.Values -contains $root) { $canonRoot = $root }
if ($canonRoot) {
$typeName = $parts[1]
$normalized = $null
if ($canonRoot -eq "Enum") {
if ($parts.Count -eq 2) {
# "Enum.X" alone — not a value, treat as string
} elseif ($parts.Count -eq 3) {
# "Enum.X.Y" — insert .EnumValue.
$normalized = "Enum.$typeName.EnumValue.$($parts[2])"
} else {
# "Enum.X.<member>.Y..." — replace member with EnumValue (handles ЗначениеПеречисления too)
$member = $parts[2]
if ($script:enumValueSynonyms -contains $member) {
$rest = $parts[3..($parts.Count-1)] -join '.'
$normalized = "Enum.$typeName.EnumValue.$rest"
} else {
$rest = $parts[2..($parts.Count-1)] -join '.'
$normalized = "Enum.$typeName.EnumValue.$rest"
}
}
} else {
# Other ref roots: just translate root, keep tail as-is
if ($parts.Count -ge 3) {
$tail = $parts[1..($parts.Count-1)] -join '.'
$normalized = "$canonRoot.$tail"
}
}
if ($normalized) {
return @{ XsiType = "xr:DesignTimeRef"; Text = $normalized }
}
}
}
return @{ XsiType = "xs:string"; Text = $s }
}
# Emit Presentation block for a choiceList item.
# Accepts string (ru only), or hashtable/PSCustomObject {ru, en, ...}.
# Empty/null → emits empty <Presentation/>.
function Emit-ChoicePresentation {
param($pres, [string]$indent)
if ($null -eq $pres -or ($pres -is [string] -and [string]::IsNullOrEmpty($pres))) {
X "$indent<Presentation/>"
return
}
$pairs = @()
if ($pres -is [string]) {
$pairs += ,@("ru", $pres)
} elseif ($pres -is [hashtable] -or $pres -is [System.Collections.IDictionary]) {
foreach ($k in $pres.Keys) { $pairs += ,@("$k", "$($pres[$k])") }
} elseif ($pres.PSObject -and $pres.PSObject.Properties) {
foreach ($p in $pres.PSObject.Properties) { $pairs += ,@("$($p.Name)", "$($p.Value)") }
} else {
$pairs += ,@("ru", "$pres")
}
X "$indent<Presentation>"
foreach ($pair in $pairs) {
X "$indent`t<v8:item>"
X "$indent`t`t<v8:lang>$($pair[0])</v8:lang>"
X "$indent`t`t<v8:content>$(Esc-Xml $pair[1])</v8:content>"
X "$indent`t</v8:item>"
}
X "$indent</Presentation>"
}
function Emit-Radio {
param($el, [string]$name, [int]$id, [string]$indent)
X "$indent<RadioButtonField name=`"$name`" id=`"$id`">"
$inner = "$indent`t"
if ($el.path) { X "$inner<DataPath>$($el.path)</DataPath>" }
Emit-Title -el $el -name $name -indent $inner -auto:(-not $el.path)
Emit-CommonFlags -el $el -indent $inner
# TitleLocation default is None for radio (matches typical configurator behavior)
$tl = if ($el.titleLocation) {
switch ("$($el.titleLocation)") {
"none" { "None" }
"left" { "Left" }
"right" { "Right" }
"top" { "Top" }
"bottom" { "Bottom" }
default { "$($el.titleLocation)" }
}
} else { "None" }
X "$inner<TitleLocation>$tl</TitleLocation>"
# RadioButtonType: Auto | RadioButtons | Tumbler. Accept synonyms.
$rbtRaw = if ($el.radioButtonType) { "$($el.radioButtonType)".Trim() } else { "Auto" }
$rbt = switch -Regex ($rbtRaw.ToLower()) {
'^(auto|авто)$' { "Auto"; break }
'^(radiobuttons?|переключатель|радио)$' { "RadioButtons"; break }
'^(tumbler|тумблер)$' { "Tumbler"; break }
default { $rbtRaw }
}
X "$inner<RadioButtonType>$rbt</RadioButtonType>"
if ($null -ne $el.columnsCount) {
X "$inner<ColumnsCount>$($el.columnsCount)</ColumnsCount>"
}
# ChoiceList
if ($el.choiceList -and $el.choiceList.Count -gt 0) {
X "$inner<ChoiceList>"
$itemIndent = "$inner`t"
foreach ($item in $el.choiceList) {
# Pull value (and tolerate Russian synonym "значение")
$valRaw = $null
if ($item -is [hashtable] -or $item -is [System.Collections.IDictionary]) {
if ($item.Contains("value")) { $valRaw = $item["value"] }
elseif ($item.Contains("значение")) { $valRaw = $item["значение"] }
} else {
if ($item.PSObject.Properties["value"]) { $valRaw = $item.value }
elseif ($item.PSObject.Properties["значение"]) { $valRaw = $item."значение" }
}
# Pull presentation (presentation OR title synonym)
$presRaw = $null
$hasPres = $false
if ($item -is [hashtable] -or $item -is [System.Collections.IDictionary]) {
if ($item.Contains("presentation")) { $presRaw = $item["presentation"]; $hasPres = $true }
elseif ($item.Contains("представление")) { $presRaw = $item["представление"]; $hasPres = $true }
elseif ($item.Contains("title")) { $presRaw = $item["title"]; $hasPres = $true }
} else {
if ($item.PSObject.Properties["presentation"]) { $presRaw = $item.presentation; $hasPres = $true }
elseif ($item.PSObject.Properties["представление"]) { $presRaw = $item."представление"; $hasPres = $true }
elseif ($item.PSObject.Properties["title"]) { $presRaw = $item.title; $hasPres = $true }
}
$norm = Normalize-ChoiceValue -value $valRaw
# Auto-derive presentation if missing
if (-not $hasPres) {
if ($norm.XsiType -eq "xr:DesignTimeRef") {
$tail = ($norm.Text -split '\.')[-1]
$presRaw = Title-FromName -name $tail
} elseif ($norm.XsiType -eq "xs:string") {
$presRaw = $norm.Text
} else {
$presRaw = $norm.Text
}
}
X "$itemIndent<xr:Item>"
$valIndent = "$itemIndent`t"
X "$valIndent<xr:Presentation/>"
X "$valIndent<xr:CheckState>0</xr:CheckState>"
X "$valIndent<xr:Value xsi:type=`"FormChoiceListDesTimeValue`">"
Emit-ChoicePresentation -pres $presRaw -indent "$valIndent`t"
X "$valIndent`t<Value xsi:type=`"$($norm.XsiType)`">$(Esc-Xml $norm.Text)</Value>"
X "$valIndent</xr:Value>"
X "$itemIndent</xr:Item>"
}
X "$inner</ChoiceList>"
}
# Companions
Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner
Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner
Emit-Events -el $el -elementName $name -indent $inner -typeKey "radio"
X "$indent</RadioButtonField>"
}
function Emit-Label {
param($el, [string]$name, [int]$id, [string]$indent)
X "$indent<LabelDecoration name=`"$name`" id=`"$id`">"
$inner = "$indent`t"
if ($el.title) {
$labelTitle = if ($el.title) { "$($el.title)" } else { Title-FromName -name $name }
if ($labelTitle) {
$formatted = if ($el.hyperlink -eq $true) { "true" } else { "false" }
X "$inner<Title formatted=`"$formatted`">"
X "$inner`t<v8:item>"
X "$inner`t`t<v8:lang>ru</v8:lang>"
X "$inner`t`t<v8:content>$(Esc-Xml "$($el.title)")</v8:content>"
X "$inner`t`t<v8:content>$(Esc-Xml "$labelTitle")</v8:content>"
X "$inner`t</v8:item>"
X "$inner</Title>"
}
@@ -2075,7 +2427,9 @@ function Emit-Label {
if ($el.hyperlink -eq $true) { X "$inner<Hyperlink>true</Hyperlink>" }
if ($el.autoMaxWidth -eq $false) { X "$inner<AutoMaxWidth>false</AutoMaxWidth>" }
if ($null -ne $el.maxWidth) { X "$inner<MaxWidth>$($el.maxWidth)</MaxWidth>" }
if ($el.autoMaxHeight -eq $false) { X "$inner<AutoMaxHeight>false</AutoMaxHeight>" }
if ($null -ne $el.maxHeight) { X "$inner<MaxHeight>$($el.maxHeight)</MaxHeight>" }
if ($el.width) { X "$inner<Width>$($el.width)</Width>" }
if ($el.height) { X "$inner<Height>$($el.height)</Height>" }
@@ -2096,7 +2450,7 @@ function Emit-LabelField {
if ($el.path) { X "$inner<DataPath>$($el.path)</DataPath>" }
Emit-Title -el $el -name $name -indent $inner
Emit-Title -el $el -name $name -indent $inner -auto:(-not $el.path)
Emit-CommonFlags -el $el -indent $inner
if ($el.hyperlink -eq $true) { X "$inner<Hyperlink>true</Hyperlink>" }
@@ -2118,7 +2472,7 @@ function Emit-Table {
if ($el.path) { X "$inner<DataPath>$($el.path)</DataPath>" }
Emit-Title -el $el -name $name -indent $inner
Emit-Title -el $el -name $name -indent $inner -auto:(-not $el.path)
Emit-CommonFlags -el $el -indent $inner
if ($el.representation) {
@@ -2207,7 +2561,7 @@ function Emit-Page {
X "$indent<Page name=`"$name`" id=`"$id`">"
$inner = "$indent`t"
Emit-Title -el $el -name $name -indent $inner
Emit-Title -el $el -name $name -indent $inner -auto
Emit-CommonFlags -el $el -indent $inner
if ($el.group) {
@@ -2237,19 +2591,48 @@ function Emit-Page {
}
function Emit-Button {
param($el, [string]$name, [int]$id, [string]$indent)
param($el, [string]$name, [int]$id, [string]$indent, [bool]$inCmdBar = $false)
X "$indent<Button name=`"$name`" id=`"$id`">"
$inner = "$indent`t"
# Type
# Type — context-aware:
# Inside command bar (cmdBar/autoCmdBar/popup) only CommandBarButton/CommandBarHyperlink are valid.
# UsualButton/Hyperlink would be silently ignored by 1C.
$btnType = $null
if ($el.type) {
$btnType = switch ("$($el.type)") {
"usual" { "UsualButton" }
"hyperlink" { "Hyperlink" }
"commandBar" { "CommandBarButton" }
default { "$($el.type)" }
$rawType = "$($el.type)"
if ($inCmdBar) {
# Be forgiving: any "ordinary button" hint resolves to CommandBarButton,
# any "hyperlink" hint resolves to CommandBarHyperlink. The model can pass
# either DSL ("usual"/"hyperlink") or XML names — all map to the right kind.
switch ($rawType) {
"usual" { $btnType = "CommandBarButton" }
"UsualButton" { $btnType = "CommandBarButton" }
"commandBar" { $btnType = "CommandBarButton" }
"CommandBarButton" { $btnType = "CommandBarButton" }
"hyperlink" { $btnType = "CommandBarHyperlink" }
"Hyperlink" { $btnType = "CommandBarHyperlink" }
"CommandBarHyperlink" { $btnType = "CommandBarHyperlink" }
default { $btnType = $rawType }
}
} else {
# Symmetric: any "ordinary button" hint → UsualButton, any "hyperlink" → Hyperlink.
switch ($rawType) {
"usual" { $btnType = "UsualButton" }
"UsualButton" { $btnType = "UsualButton" }
"commandBar" { $btnType = "UsualButton" }
"CommandBarButton" { $btnType = "UsualButton" }
"hyperlink" { $btnType = "Hyperlink" }
"Hyperlink" { $btnType = "Hyperlink" }
"CommandBarHyperlink" { $btnType = "Hyperlink" }
default { $btnType = $rawType }
}
}
} elseif ($inCmdBar) {
$btnType = "CommandBarButton"
}
if ($btnType) {
X "$inner<Type>$btnType</Type>"
}
@@ -2266,7 +2649,8 @@ function Emit-Button {
}
}
Emit-Title -el $el -name $name -indent $inner
$btnAuto = -not ($el.command -or $el.stdCommand)
Emit-Title -el $el -name $name -indent $inner -auto:$btnAuto
Emit-CommonFlags -el $el -indent $inner
if ($el.defaultButton -eq $true) { X "$inner<DefaultButton>true</DefaultButton>" }
@@ -2356,7 +2740,7 @@ function Emit-Calendar {
if ($el.path) { X "$inner<DataPath>$($el.path)</DataPath>" }
Emit-Title -el $el -name $name -indent $inner
Emit-Title -el $el -name $name -indent $inner -auto:(-not $el.path)
Emit-CommonFlags -el $el -indent $inner
# Companions
@@ -2382,7 +2766,7 @@ function Emit-CommandBar {
if ($el.children -and $el.children.Count -gt 0) {
X "$inner<ChildItems>"
foreach ($child in $el.children) {
Emit-Element -el $child -indent "$inner`t"
Emit-Element -el $child -indent "$inner`t" -inCmdBar $true
}
X "$inner</ChildItems>"
}
@@ -2396,7 +2780,7 @@ function Emit-Popup {
X "$indent<Popup name=`"$name`" id=`"$id`">"
$inner = "$indent`t"
Emit-Title -el $el -name $name -indent $inner
Emit-Title -el $el -name $name -indent $inner -auto
Emit-CommonFlags -el $el -indent $inner
if ($el.picture) {
@@ -2414,7 +2798,7 @@ function Emit-Popup {
if ($el.children -and $el.children.Count -gt 0) {
X "$inner<ChildItems>"
foreach ($child in $el.children) {
Emit-Element -el $child -indent "$inner`t"
Emit-Element -el $child -indent "$inner`t" -inCmdBar $true
}
X "$inner</ChildItems>"
}
@@ -2437,8 +2821,9 @@ function Emit-Attributes {
X "$indent`t<Attribute name=`"$attrName`" id=`"$attrId`">"
$inner = "$indent`t`t"
if ($attr.title) {
Emit-MLText -tag "Title" -text "$($attr.title)" -indent $inner
$attrTitle = if ($attr.title) { "$($attr.title)" } elseif ($attr.main -ne $true) { Title-FromName -name $attrName } else { '' }
if ($attrTitle) {
Emit-MLText -tag "Title" -text "$attrTitle" -indent $inner
}
# Type
@@ -2451,7 +2836,11 @@ function Emit-Attributes {
if ($attr.main -eq $true) {
X "$inner<MainAttribute>true</MainAttribute>"
}
if ($attr.savedData -eq $true) {
$mainSaved = $false
if ($attr.main -eq $true -and $attr.type) {
$mainSaved = ("$($attr.type)") -match '^(CatalogObject|DocumentObject|ChartOfAccountsObject|ChartOfCalculationTypesObject|ChartOfCharacteristicTypesObject|ExchangePlanObject|BusinessProcessObject|TaskObject)\.' -or ("$($attr.type)") -match 'RecordManager\.'
}
if ($attr.savedData -eq $true -or $mainSaved) {
X "$inner<SavedData>true</SavedData>"
}
if ($attr.fillChecking) {
@@ -2526,8 +2915,9 @@ function Emit-Commands {
X "$indent`t<Command name=`"$($cmd.name)`" id=`"$cmdId`">"
$inner = "$indent`t`t"
if ($cmd.title) {
Emit-MLText -tag "Title" -text "$($cmd.title)" -indent $inner
$cmdTitle = if ($cmd.title) { "$($cmd.title)" } else { Title-FromName -name "$($cmd.name)" }
if ($cmdTitle) {
Emit-MLText -tag "Title" -text "$cmdTitle" -indent $inner
}
if ($cmd.action) {
@@ -2601,6 +2991,176 @@ function Emit-Properties {
}
}
# --- 11b. Pre-pass: synonyms, main attribute inference, heuristics, autoCmdBar extraction ---
function Normalize-ElementSynonyms {
param($el)
if ($null -eq $el) { return }
$synonyms = @{ "commandBar" = "cmdBar"; "autoCommandBar" = "autoCmdBar" }
foreach ($pair in $synonyms.GetEnumerator()) {
if ($null -ne $el.PSObject.Properties[$pair.Key] -and $null -eq $el.PSObject.Properties[$pair.Value]) {
$val = $el.($pair.Key)
$el.PSObject.Properties.Remove($pair.Key) | Out-Null
$el | Add-Member -NotePropertyName $pair.Value -NotePropertyValue $val -Force
}
}
if ($el.PSObject.Properties["children"] -and $el.children) {
foreach ($child in $el.children) { Normalize-ElementSynonyms $child }
}
if ($el.PSObject.Properties["columns"] -and $el.columns) {
foreach ($child in $el.columns) { Normalize-ElementSynonyms $child }
}
}
function HasCmdBarRecursive {
param($el)
if ($null -eq $el) { return $false }
if ($el.PSObject.Properties["cmdBar"] -and $null -ne $el.cmdBar) { return $true }
if ($el.PSObject.Properties["children"] -and $el.children) {
foreach ($child in $el.children) { if (HasCmdBarRecursive $child) { return $true } }
}
if ($el.PSObject.Properties["columns"] -and $el.columns) {
foreach ($child in $el.columns) { if (HasCmdBarRecursive $child) { return $true } }
}
return $false
}
function ApplyDynamicListTableHeuristic {
param($el, [string]$listName, [bool]$hasMainTable)
if ($null -eq $el) { return }
if ($el.PSObject.Properties["table"] -and $null -ne $el.table -and "$($el.path)" -eq $listName) {
if ($null -eq $el.PSObject.Properties["tableAutofill"]) {
$el | Add-Member -NotePropertyName "tableAutofill" -NotePropertyValue $false -Force
}
if ($null -eq $el.PSObject.Properties["commandBarLocation"]) {
$el | Add-Member -NotePropertyName "commandBarLocation" -NotePropertyValue "None" -Force
}
# DefaultPicture доступен только если у DynamicList есть основная таблица
if ($hasMainTable -and ($null -eq $el.PSObject.Properties["rowPictureDataPath"] -or [string]::IsNullOrEmpty("$($el.rowPictureDataPath)"))) {
$el | Add-Member -NotePropertyName "rowPictureDataPath" -NotePropertyValue "$listName.DefaultPicture" -Force
}
}
if ($el.PSObject.Properties["children"] -and $el.children) {
foreach ($child in $el.children) { ApplyDynamicListTableHeuristic $child $listName $hasMainTable }
}
}
function Test-IsObjectLikeType {
param([string]$type)
if ([string]::IsNullOrEmpty($type)) { return $false }
if ($type -eq "DynamicList" -or $type -eq "ConstantsSet") { return $true }
$objectSuffixes = @(
"CatalogObject", "DocumentObject", "DataProcessorObject", "ReportObject",
"ExternalDataProcessorObject", "ExternalReportObject", "BusinessProcessObject",
"TaskObject", "ChartOfAccountsObject", "ChartOfCharacteristicTypesObject",
"ChartOfCalculationTypesObject", "ExchangePlanObject"
)
$recordSetPrefixes = @(
"InformationRegisterRecordSet", "AccumulationRegisterRecordSet",
"AccountingRegisterRecordSet", "CalculationRegisterRecordSet",
"InformationRegisterRecordManager"
)
foreach ($suffix in $objectSuffixes) {
if ($type -like "$suffix.*") { return $true }
}
foreach ($prefix in $recordSetPrefixes) {
if ($type -like "$prefix.*") { return $true }
}
return $false
}
# 11b.1: Normalize synonyms recursively
if ($def.elements) {
foreach ($el in $def.elements) { Normalize-ElementSynonyms $el }
}
# 11b.2: Extract autoCmdBar element from def.elements
$script:mainAcbDef = $null
if ($def.elements) {
$autoBars = @()
$rest = @()
foreach ($el in $def.elements) {
if ($null -ne $el.PSObject.Properties["autoCmdBar"] -and $null -ne $el.autoCmdBar) {
$autoBars += $el
} else {
$rest += $el
}
}
if ($autoBars.Count -gt 1) {
Write-Error "form-compile: more than one autoCmdBar in def.elements (found $($autoBars.Count)); only one allowed."
exit 1
}
if ($autoBars.Count -eq 1) {
$script:mainAcbDef = $autoBars[0]
# Replace def.elements with the filtered list
$def.PSObject.Properties.Remove("elements") | Out-Null
$def | Add-Member -NotePropertyName "elements" -NotePropertyValue $rest -Force
}
}
# 11b.3: Infer main attribute (only if no attribute has main:true)
if ($def.attributes) {
$hasExplicitMain = $false
foreach ($attr in $def.attributes) {
if ($attr.main -eq $true) { $hasExplicitMain = $true; break }
}
if (-not $hasExplicitMain) {
$candidates = @()
foreach ($attr in $def.attributes) {
# Skip if user explicitly opted out via main:false
if ($null -ne $attr.PSObject.Properties["main"] -and $attr.main -eq $false) { continue }
if (Test-IsObjectLikeType "$($attr.type)") {
$candidates += $attr
}
}
if ($candidates.Count -eq 1) {
$candidates[0] | Add-Member -NotePropertyName "main" -NotePropertyValue $true -Force
Write-Host "[INFO] Inferred main attribute: $($candidates[0].name) ($($candidates[0].type))"
} elseif ($candidates.Count -gt 1) {
$names = ($candidates | ForEach-Object { $_.name }) -join ", "
Write-Host "[WARN] Multiple main-attribute candidates: $names; specify ""main"": true explicitly"
}
}
}
# 11b.4: DynamicList → table heuristic
if ($def.attributes -and $def.elements) {
$mainAttr = $null
foreach ($attr in $def.attributes) {
if ($attr.main -eq $true) { $mainAttr = $attr; break }
}
if ($mainAttr -and "$($mainAttr.type)" -eq "DynamicList") {
$mt = $null
if ($mainAttr.PSObject.Properties["settings"] -and $null -ne $mainAttr.settings) {
if ($mainAttr.settings -is [hashtable]) {
if ($mainAttr.settings.ContainsKey("mainTable")) { $mt = $mainAttr.settings["mainTable"] }
} elseif ($mainAttr.settings.PSObject.Properties["mainTable"]) {
$mt = $mainAttr.settings.mainTable
}
}
$hasMt = -not [string]::IsNullOrEmpty("$mt")
foreach ($el in $def.elements) {
ApplyDynamicListTableHeuristic $el $mainAttr.name $hasMt
}
}
}
# 11b.5: Compute main AutoCommandBar Autofill via heuristic B3
function Compute-MainAcbAutofill {
if ($script:mainAcbDef) {
if ($null -ne $script:mainAcbDef.PSObject.Properties["autofill"]) {
return [bool]$script:mainAcbDef.autofill
}
return $true
}
if ($def.elements) {
foreach ($el in $def.elements) {
if (HasCmdBarRecursive $el) { return $false }
}
}
return $true
}
# --- 12. Main compilation ---
# Title
@@ -2632,17 +3192,26 @@ if ($formTitle) {
}
# 12b. Properties (skip 'title' — handled above as multilingual)
# When form-level Title is set, default autoTitle=false (≈95% of ERP forms do this;
# otherwise platform appends synonym → "Title: Synonym" double-titles).
$propsClone = New-Object PSObject
$hasAutoTitle = $false
if ($def.properties) {
foreach ($p in $def.properties.PSObject.Properties) {
if ($p.Name -eq "autoTitle") { $hasAutoTitle = $true }
}
}
if ($formTitle -and -not $hasAutoTitle) {
$propsClone | Add-Member -NotePropertyName "autoTitle" -NotePropertyValue $false
}
if ($def.properties) {
$propsClone = New-Object PSObject
foreach ($p in $def.properties.PSObject.Properties) {
if ($p.Name -ne "title") {
$propsClone | Add-Member -NotePropertyName $p.Name -NotePropertyValue $p.Value
}
}
Emit-Properties -props $propsClone -indent "`t"
} else {
Emit-Properties -props $null -indent "`t"
}
Emit-Properties -props $propsClone -indent "`t"
# 12c. CommandSet (excluded commands)
if ($def.excludedCommands -and $def.excludedCommands.Count -gt 0) {
@@ -2654,10 +3223,37 @@ if ($def.excludedCommands -and $def.excludedCommands.Count -gt 0) {
}
# 12d. AutoCommandBar (always present, id=-1)
X "`t<AutoCommandBar name=`"ФормаКоманднаяПанель`" id=`"-1`">"
X "`t`t<HorizontalAlign>Right</HorizontalAlign>"
X "`t`t<Autofill>false</Autofill>"
X "`t</AutoCommandBar>"
$acbAutofill = Compute-MainAcbAutofill
$acbName = "ФормаКоманднаяПанель"
$acbHAlign = $null
if ($script:mainAcbDef) {
if ($null -ne $script:mainAcbDef.PSObject.Properties["autoCmdBar"] -and "$($script:mainAcbDef.autoCmdBar)" -ne "") {
$acbName = "$($script:mainAcbDef.autoCmdBar)"
}
if ($null -ne $script:mainAcbDef.PSObject.Properties["name"] -and "$($script:mainAcbDef.name)" -ne "") {
$acbName = "$($script:mainAcbDef.name)"
}
if ($null -ne $script:mainAcbDef.PSObject.Properties["horizontalAlign"] -and "$($script:mainAcbDef.horizontalAlign)" -ne "") {
$acbHAlign = "$($script:mainAcbDef.horizontalAlign)"
}
}
$hasAcbChildren = ($script:mainAcbDef -and $script:mainAcbDef.children -and $script:mainAcbDef.children.Count -gt 0)
$acbHasInner = ($acbHAlign -or (-not $acbAutofill) -or $hasAcbChildren)
if ($acbHasInner) {
X "`t<AutoCommandBar name=`"$acbName`" id=`"-1`">"
if ($acbHAlign) { X "`t`t<HorizontalAlign>$acbHAlign</HorizontalAlign>" }
if (-not $acbAutofill) { X "`t`t<Autofill>false</Autofill>" }
if ($hasAcbChildren) {
X "`t`t<ChildItems>"
foreach ($child in $script:mainAcbDef.children) {
Emit-Element -el $child -indent "`t`t`t" -inCmdBar $true
}
X "`t`t</ChildItems>"
}
X "`t</AutoCommandBar>"
} else {
X "`t<AutoCommandBar name=`"$acbName`" id=`"-1`"/>"
}
# 12e. Events
if ($def.events) {
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# form-compile v1.6 — Compile 1C managed form from JSON or object metadata
# form-compile v1.20 — Compile 1C managed form from JSON or object metadata
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import copy
@@ -625,6 +625,7 @@ def generate_document_list_dsl(meta, p):
table_el = OrderedDict([
('table', '\u0421\u043f\u0438\u0441\u043e\u043a'), ('path', '\u0421\u043f\u0438\u0441\u043e\u043a'),
('rowPictureDataPath', '\u0421\u043f\u0438\u0441\u043e\u043a.DefaultPicture'),
('commandBarLocation', 'None'),
('tableAutofill', False),
('columns', columns),
@@ -938,6 +939,7 @@ def generate_information_register_list_dsl(meta, p):
table_el = OrderedDict([
('table', '\u0421\u043f\u0438\u0441\u043e\u043a'),
('path', '\u0421\u043f\u0438\u0441\u043e\u043a'),
('rowPictureDataPath', '\u0421\u043f\u0438\u0441\u043e\u043a.DefaultPicture'),
('commandBarLocation', 'None'),
('tableAutofill', False),
('columns', columns_list),
@@ -996,6 +998,7 @@ def generate_accumulation_register_list_dsl(meta, p):
table_el = OrderedDict([
('table', '\u0421\u043f\u0438\u0441\u043e\u043a'),
('path', '\u0421\u043f\u0438\u0441\u043e\u043a'),
('rowPictureDataPath', '\u0421\u043f\u0438\u0441\u043e\u043a.DefaultPicture'),
('commandBarLocation', 'None'),
('tableAutofill', False),
('columns', columns_list),
@@ -1311,6 +1314,7 @@ EVENT_SUFFIX_MAP = {
KNOWN_EVENTS = {
"input": ["OnChange", "StartChoice", "ChoiceProcessing", "AutoComplete", "TextEditEnd", "Clearing", "Creating", "EditTextChange"],
"check": ["OnChange"],
"radio": ["OnChange"],
"label": ["Click", "URLProcessing"],
"labelField": ["OnChange", "StartChoice", "ChoiceProcessing", "Click", "URLProcessing", "Clearing"],
"table": ["Selection", "BeforeAddRow", "AfterDeleteRow", "BeforeDeleteRow", "OnActivateRow", "OnEditEnd", "OnStartEdit", "BeforeRowChange", "BeforeEditEnd", "ValueChoice", "OnActivateCell", "OnActivateField", "Drag", "DragStart", "DragCheck", "DragEnd", "OnGetDataAtServer", "BeforeLoadUserSettingsAtServer", "OnUpdateUserSettingSetAtServer", "OnChange"],
@@ -1334,17 +1338,20 @@ KNOWN_FORM_EVENTS = [
]
KNOWN_KEYS = {
"group", "input", "check", "label", "labelField", "table", "pages", "page",
"group", "columnGroup", "input", "check", "radio", "label", "labelField", "table", "pages", "page",
"button", "picture", "picField", "calendar", "cmdBar", "popup",
"showInHeader",
"radioButtonType", "choiceList", "columnsCount",
"name", "path", "title",
"visible", "hidden", "enabled", "disabled", "readOnly", "userVisible",
"on", "handlers",
"titleLocation", "representation", "width", "height",
"horizontalStretch", "verticalStretch", "autoMaxWidth", "autoMaxHeight",
"maxWidth", "maxHeight",
"multiLine", "passwordMode", "choiceButton", "clearButton",
"spinButton", "dropListButton", "markIncomplete", "skipOnInput", "inputHint",
"hyperlink",
"showTitle", "united",
"showTitle", "united", "collapsed",
"children", "columns",
"changeRowSet", "changeRowOrder", "header", "footer",
"commandBarLocation", "searchStringLocation",
@@ -1356,9 +1363,147 @@ KNOWN_KEYS = {
"rowPictureDataPath", "tableAutofill",
}
TYPE_KEYS = ["group", "input", "check", "label", "labelField", "table", "pages", "page",
TYPE_KEYS = ["columnGroup", "group", "input", "check", "radio", "label", "labelField", "table", "pages", "page",
"button", "picture", "picField", "calendar", "cmdBar", "popup"]
# Synonyms: model often writes XML name or Russian (ПолеПереключателя/RadioButtonField → radio)
ELEMENT_TYPE_SYNONYMS = {
"commandBar": "cmdBar",
"autoCommandBar": "autoCmdBar",
"КоманднаяПанель": "cmdBar",
"InputField": "input",
"ПолеВвода": "input",
"CheckBoxField": "check",
"ПолеФлажка": "check",
"RadioButtonField": "radio",
"ПолеПереключателя": "radio",
"radioButton": "radio",
"PictureField": "picField",
"ПолеКартинки": "picField",
"LabelField": "labelField",
"ПолеНадписи": "labelField",
"CalendarField": "calendar",
"ПолеКалендаря": "calendar",
"LabelDecoration": "label",
"Надпись": "label",
"PictureDecoration": "picture",
"Картинка": "picture",
"UsualGroup": "group",
"Группа": "group",
"ОбычнаяГруппа": "group",
"ColumnGroup": "columnGroup",
"ГруппаКолонок": "columnGroup",
"Pages": "pages",
"ГруппаСтраниц": "pages",
"Page": "page",
"Страница": "page",
"Table": "table",
"Таблица": "table",
"Button": "button",
"Кнопка": "button",
"Popup": "popup",
"ВсплывающееМеню": "popup",
}
# Maps Russian/English root of typed reference path to canonical English root
REF_ROOT_SYNONYMS = {
"Перечисление": "Enum",
"Справочник": "Catalog",
"Документ": "Document",
"ПланСчетов": "ChartOfAccounts",
"ПланВидовХарактеристик": "ChartOfCharacteristicTypes",
"ПланВидовРасчета": "ChartOfCalculationTypes",
"ПланВидовРасчёта": "ChartOfCalculationTypes",
"ПланОбмена": "ExchangePlan",
"БизнесПроцесс": "BusinessProcess",
"Задача": "Task",
"РегистрСведений": "InformationRegister",
"РегистрНакопления": "AccumulationRegister",
"РегистрБухгалтерии": "AccountingRegister",
"РегистрРасчета": "CalculationRegister",
"РегистрРасчёта": "CalculationRegister",
}
ENUM_VALUE_SYNONYMS = {"EnumValue", "ЗначениеПеречисления"}
def normalize_choice_value(value):
"""Returns dict {xsi_type, text} for a choiceList item value."""
if isinstance(value, bool):
return {"xsi_type": "xs:boolean", "text": "true" if value else "false"}
if isinstance(value, (int, float)):
return {"xsi_type": "xs:decimal", "text": str(value)}
s = "" if value is None else str(value)
if not s:
return {"xsi_type": "xs:string", "text": ""}
parts = s.split(".")
if len(parts) >= 2:
root = parts[0]
canon_root = None
if root in REF_ROOT_SYNONYMS:
canon_root = REF_ROOT_SYNONYMS[root]
elif root in REF_ROOT_SYNONYMS.values():
canon_root = root
if canon_root:
type_name = parts[1]
normalized = None
if canon_root == "Enum":
if len(parts) == 3:
normalized = f"Enum.{type_name}.EnumValue.{parts[2]}"
elif len(parts) >= 4:
member = parts[2]
if member in ENUM_VALUE_SYNONYMS:
rest = ".".join(parts[3:])
else:
rest = ".".join(parts[2:])
normalized = f"Enum.{type_name}.EnumValue.{rest}"
else:
if len(parts) >= 3:
tail = ".".join(parts[1:])
normalized = f"{canon_root}.{tail}"
if normalized:
return {"xsi_type": "xr:DesignTimeRef", "text": normalized}
return {"xsi_type": "xs:string", "text": s}
def emit_choice_presentation(lines, pres, indent):
"""Accepts None/empty → <Presentation/>; str → ru only; dict → multi-lang."""
if pres is None or (isinstance(pres, str) and pres == ""):
lines.append(f"{indent}<Presentation/>")
return
if isinstance(pres, str):
pairs = [("ru", pres)]
elif isinstance(pres, dict):
pairs = [(str(k), str(v)) for k, v in pres.items()]
else:
pairs = [("ru", str(pres))]
lines.append(f"{indent}<Presentation>")
for lang, content in pairs:
lines.append(f"{indent}\t<v8:item>")
lines.append(f"{indent}\t\t<v8:lang>{lang}</v8:lang>")
lines.append(f"{indent}\t\t<v8:content>{esc_xml(content)}</v8:content>")
lines.append(f"{indent}\t</v8:item>")
lines.append(f"{indent}</Presentation>")
def normalize_radio_button_type(raw):
if not raw:
return "Auto"
s = str(raw).strip().lower()
if s in ("auto", "авто"):
return "Auto"
if s in ("radiobutton", "radiobuttons", "переключатель", "радио"):
return "RadioButtons"
if s in ("tumbler", "тумблер"):
return "Tumbler"
return str(raw).strip()
def get_handler_name(element_name, event_name):
suffix = EVENT_SUFFIX_MAP.get(event_name)
@@ -1414,9 +1559,27 @@ def emit_common_flags(lines, el, indent):
lines.append(f"{indent}<ReadOnly>true</ReadOnly>")
def emit_title(lines, el, name, indent):
if el.get('title'):
emit_mltext(lines, indent, 'Title', str(el['title']))
def title_from_name(name):
"""СуммаДокумента → 'Сумма документа'. НДСВключен → 'НДС включен'."""
if not name:
return ''
s = re.sub(r'([А-ЯA-Z])([А-ЯA-Z][а-яa-z])', r'\1 \2', name)
s = re.sub(r'([а-яa-z0-9])([А-ЯA-Z])', r'\1 \2', s)
parts = s.split(' ')
if not parts:
return s
out = [parts[0]]
for p in parts[1:]:
out.append(p if (len(p) > 1 and p.isupper()) else p.lower())
return ' '.join(out)
def emit_title(lines, el, name, indent, auto=False):
title = el.get('title')
if not title and auto and name:
title = title_from_name(name)
if title:
emit_mltext(lines, indent, 'Title', str(title))
# --- Type emitter ---
@@ -1455,7 +1618,7 @@ CFG_REF_PATTERN = re.compile(
)
KNOWN_INVALID_TYPES = {
'FormDataStructure': 'Runtime type. Use cfg:*Object.XXX (e.g. CatalogObject.XXX)',
'FormDataStructure': 'Runtime type. Use object type without cfg: prefix (e.g. CatalogObject.Контрагенты, DocumentObject.Приход)',
'FormDataCollection': 'Runtime type. Use ValueTable',
'FormDataTree': 'Runtime type. Use ValueTree',
'FormDataTreeItem': 'Runtime type, not valid in XML',
@@ -1489,6 +1652,9 @@ _FORM_TYPE_SYNONYMS = {
def resolve_type_str(type_str):
if not type_str:
return type_str
# Lenient: strip leading cfg: prefix if user passed it (canonical form is without prefix)
if type_str.startswith('cfg:'):
type_str = type_str[4:]
m = re.match(r'^([^(]+)\((.+)\)$', type_str)
if m:
base, params = m.group(1).strip(), m.group(2)
@@ -1598,7 +1764,12 @@ def emit_type(lines, type_str, indent):
# --- Element emitters ---
def emit_element(lines, el, indent):
def emit_element(lines, el, indent, in_cmd_bar=False):
# Silent synonyms: model often writes XML name or Russian (ПолеПереключателя/RadioButtonField → radio)
for src, dst in ELEMENT_TYPE_SYNONYMS.items():
if src in el and dst not in el:
el[dst] = el.pop(src)
type_key = None
for key in TYPE_KEYS:
if el.get(key) is not None:
@@ -1619,8 +1790,10 @@ def emit_element(lines, el, indent):
emitters = {
'group': emit_group,
'columnGroup': emit_column_group,
'input': emit_input,
'check': emit_check,
'radio': emit_radio_button_field,
'label': emit_label,
'labelField': emit_label_field,
'table': emit_table,
@@ -1636,7 +1809,10 @@ def emit_element(lines, el, indent):
emitter = emitters.get(type_key)
if emitter:
emitter(lines, el, name, eid, indent)
if type_key == 'button':
emitter(lines, el, name, eid, indent, in_cmd_bar=in_cmd_bar)
else:
emitter(lines, el, name, eid, indent)
def emit_group(lines, el, name, eid, indent):
@@ -1661,6 +1837,8 @@ def emit_group(lines, el, name, eid, indent):
if group_val == 'collapsible':
lines.append(f'{inner}<Group>Vertical</Group>')
lines.append(f'{inner}<Behavior>Collapsible</Behavior>')
if el.get('collapsed') is True:
lines.append(f'{inner}<Collapsed>true</Collapsed>')
# Representation
if el.get('representation'):
@@ -1696,6 +1874,43 @@ def emit_group(lines, el, name, eid, indent):
lines.append(f'{indent}</UsualGroup>')
def emit_column_group(lines, el, name, eid, indent):
lines.append(f'{indent}<ColumnGroup name="{name}" id="{eid}">')
inner = f'{indent}\t'
emit_title(lines, el, name, inner)
group_val = str(el.get('columnGroup', ''))
orientation_map = {
'horizontal': 'Horizontal',
'vertical': 'Vertical',
'inCell': 'InCell',
}
orientation = orientation_map.get(group_val)
if orientation:
lines.append(f'{inner}<Group>{orientation}</Group>')
if el.get('showTitle') is False:
lines.append(f'{inner}<ShowTitle>false</ShowTitle>')
if el.get('showInHeader') is not None:
sh_val = 'true' if el['showInHeader'] else 'false'
lines.append(f'{inner}<ShowInHeader>{sh_val}</ShowInHeader>')
if el.get('width'):
lines.append(f'{inner}<Width>{el["width"]}</Width>')
emit_common_flags(lines, el, inner)
emit_companion(lines, 'ExtendedTooltip', f'{name}РасширеннаяПодсказка', inner)
if el.get('children') and len(el['children']) > 0:
lines.append(f'{inner}<ChildItems>')
for child in el['children']:
emit_element(lines, child, f'{inner}\t')
lines.append(f'{inner}</ChildItems>')
lines.append(f'{indent}</ColumnGroup>')
def emit_input(lines, el, name, eid, indent):
lines.append(f'{indent}<InputField name="{name}" id="{eid}">')
inner = f'{indent}\t'
@@ -1703,7 +1918,7 @@ def emit_input(lines, el, name, eid, indent):
if el.get('path'):
lines.append(f'{inner}<DataPath>{el["path"]}</DataPath>')
emit_title(lines, el, name, inner)
emit_title(lines, el, name, inner, auto=not el.get('path'))
emit_common_flags(lines, el, inner)
if el.get('titleLocation'):
@@ -1727,10 +1942,17 @@ def emit_input(lines, el, name, eid, indent):
lines.append(f'{inner}<AutoMarkIncomplete>true</AutoMarkIncomplete>')
if el.get('skipOnInput') is True:
lines.append(f'{inner}<SkipOnInput>true</SkipOnInput>')
if el.get('autoMaxWidth') is False:
if 'autoMaxWidth' in el:
if el['autoMaxWidth'] is False:
lines.append(f'{inner}<AutoMaxWidth>false</AutoMaxWidth>')
elif el.get('multiLine') is True:
lines.append(f'{inner}<AutoMaxWidth>false</AutoMaxWidth>')
if el.get('maxWidth') is not None:
lines.append(f'{inner}<MaxWidth>{el["maxWidth"]}</MaxWidth>')
if el.get('autoMaxHeight') is False:
lines.append(f'{inner}<AutoMaxHeight>false</AutoMaxHeight>')
if el.get('maxHeight') is not None:
lines.append(f'{inner}<MaxHeight>{el["maxHeight"]}</MaxHeight>')
if el.get('width'):
lines.append(f'{inner}<Width>{el["width"]}</Width>')
if el.get('height'):
@@ -1759,11 +1981,11 @@ def emit_check(lines, el, name, eid, indent):
if el.get('path'):
lines.append(f'{inner}<DataPath>{el["path"]}</DataPath>')
emit_title(lines, el, name, inner)
emit_title(lines, el, name, inner, auto=not el.get('path'))
emit_common_flags(lines, el, inner)
if el.get('titleLocation'):
lines.append(f'{inner}<TitleLocation>{el["titleLocation"]}</TitleLocation>')
tl = el.get('titleLocation') or 'Right'
lines.append(f'{inner}<TitleLocation>{tl}</TitleLocation>')
# Companions
emit_companion(lines, 'ContextMenu', f'{name}\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435\u041c\u0435\u043d\u044e', inner)
@@ -1774,16 +1996,80 @@ def emit_check(lines, el, name, eid, indent):
lines.append(f'{indent}</CheckBoxField>')
def emit_radio_button_field(lines, el, name, eid, indent):
lines.append(f'{indent}<RadioButtonField name="{name}" id="{eid}">')
inner = f'{indent}\t'
if el.get('path'):
lines.append(f'{inner}<DataPath>{el["path"]}</DataPath>')
emit_title(lines, el, name, inner, auto=not el.get('path'))
emit_common_flags(lines, el, inner)
tl_raw = el.get('titleLocation')
if tl_raw:
loc_map = {'none': 'None', 'left': 'Left', 'right': 'Right', 'top': 'Top', 'bottom': 'Bottom'}
tl = loc_map.get(str(tl_raw), str(tl_raw))
else:
tl = 'None'
lines.append(f'{inner}<TitleLocation>{tl}</TitleLocation>')
rbt = normalize_radio_button_type(el.get('radioButtonType'))
lines.append(f'{inner}<RadioButtonType>{rbt}</RadioButtonType>')
if el.get('columnsCount') is not None:
lines.append(f'{inner}<ColumnsCount>{el["columnsCount"]}</ColumnsCount>')
choice_list = el.get('choiceList') or []
if choice_list:
lines.append(f'{inner}<ChoiceList>')
item_indent = f'{inner}\t'
for item in choice_list:
if not isinstance(item, dict):
continue
val_raw = item.get('value', item.get('значение'))
has_pres = any(k in item for k in ('presentation', 'представление', 'title'))
pres_raw = item.get('presentation', item.get('представление', item.get('title')))
norm = normalize_choice_value(val_raw)
if not has_pres:
if norm['xsi_type'] == 'xr:DesignTimeRef':
tail = norm['text'].split('.')[-1]
pres_raw = title_from_name(tail)
else:
pres_raw = norm['text']
lines.append(f'{item_indent}<xr:Item>')
val_indent = f'{item_indent}\t'
lines.append(f'{val_indent}<xr:Presentation/>')
lines.append(f'{val_indent}<xr:CheckState>0</xr:CheckState>')
lines.append(f'{val_indent}<xr:Value xsi:type="FormChoiceListDesTimeValue">')
emit_choice_presentation(lines, pres_raw, f'{val_indent}\t')
lines.append(f'{val_indent}\t<Value xsi:type="{norm["xsi_type"]}">{esc_xml(norm["text"])}</Value>')
lines.append(f'{val_indent}</xr:Value>')
lines.append(f'{item_indent}</xr:Item>')
lines.append(f'{inner}</ChoiceList>')
emit_companion(lines, 'ContextMenu', f'{name}КонтекстноеМеню', inner)
emit_companion(lines, 'ExtendedTooltip', f'{name}РасширеннаяПодсказка', inner)
emit_events(lines, el, name, inner, 'radio')
lines.append(f'{indent}</RadioButtonField>')
def emit_label(lines, el, name, eid, indent):
lines.append(f'{indent}<LabelDecoration name="{name}" id="{eid}">')
inner = f'{indent}\t'
if el.get('title'):
label_title = el.get('title') or title_from_name(name)
if label_title:
formatted = 'true' if el.get('hyperlink') is True else 'false'
lines.append(f'{inner}<Title formatted="{formatted}">')
lines.append(f'{inner}\t<v8:item>')
lines.append(f'{inner}\t\t<v8:lang>ru</v8:lang>')
lines.append(f'{inner}\t\t<v8:content>{esc_xml(str(el["title"]))}</v8:content>')
lines.append(f'{inner}\t\t<v8:content>{esc_xml(str(label_title))}</v8:content>')
lines.append(f'{inner}\t</v8:item>')
lines.append(f'{inner}</Title>')
@@ -1793,8 +2079,12 @@ def emit_label(lines, el, name, eid, indent):
lines.append(f'{inner}<Hyperlink>true</Hyperlink>')
if el.get('autoMaxWidth') is False:
lines.append(f'{inner}<AutoMaxWidth>false</AutoMaxWidth>')
if el.get('maxWidth') is not None:
lines.append(f'{inner}<MaxWidth>{el["maxWidth"]}</MaxWidth>')
if el.get('autoMaxHeight') is False:
lines.append(f'{inner}<AutoMaxHeight>false</AutoMaxHeight>')
if el.get('maxHeight') is not None:
lines.append(f'{inner}<MaxHeight>{el["maxHeight"]}</MaxHeight>')
if el.get('width'):
lines.append(f'{inner}<Width>{el["width"]}</Width>')
if el.get('height'):
@@ -1816,7 +2106,7 @@ def emit_label_field(lines, el, name, eid, indent):
if el.get('path'):
lines.append(f'{inner}<DataPath>{el["path"]}</DataPath>')
emit_title(lines, el, name, inner)
emit_title(lines, el, name, inner, auto=not el.get('path'))
emit_common_flags(lines, el, inner)
if el.get('hyperlink') is True:
@@ -1838,7 +2128,7 @@ def emit_table(lines, el, name, eid, indent):
if el.get('path'):
lines.append(f'{inner}<DataPath>{el["path"]}</DataPath>')
emit_title(lines, el, name, inner)
emit_title(lines, el, name, inner, auto=not el.get('path'))
emit_common_flags(lines, el, inner)
if el.get('representation'):
@@ -1926,7 +2216,7 @@ def emit_page(lines, el, name, eid, indent):
lines.append(f'{indent}<Page name="{name}" id="{eid}">')
inner = f'{indent}\t'
emit_title(lines, el, name, inner)
emit_title(lines, el, name, inner, auto=True)
emit_common_flags(lines, el, inner)
if el.get('group'):
@@ -1953,14 +2243,42 @@ def emit_page(lines, el, name, eid, indent):
lines.append(f'{indent}</Page>')
def emit_button(lines, el, name, eid, indent):
def emit_button(lines, el, name, eid, indent, in_cmd_bar=False):
lines.append(f'{indent}<Button name="{name}" id="{eid}">')
inner = f'{indent}\t'
# Type
# Type — context-aware. Inside command bars (cmdBar/autoCmdBar/popup) only
# CommandBarButton/CommandBarHyperlink are valid; UsualButton/Hyperlink would be ignored.
# Forgiving resolver: any "ordinary button" hint resolves to UsualButton/CommandBarButton,
# any "hyperlink" hint resolves to Hyperlink/CommandBarHyperlink — depending on context.
btn_type = None
if el.get('type'):
btn_type_map = {'usual': 'UsualButton', 'hyperlink': 'Hyperlink', 'commandBar': 'CommandBarButton'}
btn_type = btn_type_map.get(str(el['type']), str(el['type']))
raw = str(el['type'])
if in_cmd_bar:
cmd_bar_map = {
'usual': 'CommandBarButton',
'UsualButton': 'CommandBarButton',
'commandBar': 'CommandBarButton',
'CommandBarButton': 'CommandBarButton',
'hyperlink': 'CommandBarHyperlink',
'Hyperlink': 'CommandBarHyperlink',
'CommandBarHyperlink': 'CommandBarHyperlink',
}
btn_type = cmd_bar_map.get(raw, raw)
else:
normal_map = {
'usual': 'UsualButton',
'UsualButton': 'UsualButton',
'commandBar': 'UsualButton',
'CommandBarButton': 'UsualButton',
'hyperlink': 'Hyperlink',
'Hyperlink': 'Hyperlink',
'CommandBarHyperlink': 'Hyperlink',
}
btn_type = normal_map.get(raw, raw)
elif in_cmd_bar:
btn_type = 'CommandBarButton'
if btn_type:
lines.append(f'{inner}<Type>{btn_type}</Type>')
# CommandName
@@ -1974,7 +2292,7 @@ def emit_button(lines, el, name, eid, indent):
else:
lines.append(f'{inner}<CommandName>Form.StandardCommand.{sc}</CommandName>')
emit_title(lines, el, name, inner)
emit_title(lines, el, name, inner, auto=not (el.get('command') or el.get('stdCommand')))
emit_common_flags(lines, el, inner)
if el.get('defaultButton') is True:
@@ -2062,7 +2380,7 @@ def emit_calendar(lines, el, name, eid, indent):
if el.get('path'):
lines.append(f'{inner}<DataPath>{el["path"]}</DataPath>')
emit_title(lines, el, name, inner)
emit_title(lines, el, name, inner, auto=not el.get('path'))
emit_common_flags(lines, el, inner)
# Companions
@@ -2087,7 +2405,7 @@ def emit_command_bar(lines, el, name, eid, indent):
if el.get('children') and len(el['children']) > 0:
lines.append(f'{inner}<ChildItems>')
for child in el['children']:
emit_element(lines, child, f'{inner}\t')
emit_element(lines, child, f'{inner}\t', in_cmd_bar=True)
lines.append(f'{inner}</ChildItems>')
lines.append(f'{indent}</CommandBar>')
@@ -2097,7 +2415,7 @@ def emit_popup(lines, el, name, eid, indent):
lines.append(f'{indent}<Popup name="{name}" id="{eid}">')
inner = f'{indent}\t'
emit_title(lines, el, name, inner)
emit_title(lines, el, name, inner, auto=True)
emit_common_flags(lines, el, inner)
if el.get('picture'):
@@ -2113,7 +2431,7 @@ def emit_popup(lines, el, name, eid, indent):
if el.get('children') and len(el['children']) > 0:
lines.append(f'{inner}<ChildItems>')
for child in el['children']:
emit_element(lines, child, f'{inner}\t')
emit_element(lines, child, f'{inner}\t', in_cmd_bar=True)
lines.append(f'{inner}</ChildItems>')
lines.append(f'{indent}</Popup>')
@@ -2133,8 +2451,11 @@ def emit_attributes(lines, attrs, indent):
lines.append(f'{indent}\t<Attribute name="{attr_name}" id="{attr_id}">')
inner = f'{indent}\t\t'
if attr.get('title'):
emit_mltext(lines, inner, 'Title', str(attr['title']))
attr_title = attr.get('title')
if not attr_title and attr.get('main') is not True:
attr_title = title_from_name(attr_name)
if attr_title:
emit_mltext(lines, inner, 'Title', str(attr_title))
# Type
if attr.get('type'):
@@ -2144,7 +2465,11 @@ def emit_attributes(lines, attrs, indent):
if attr.get('main') is True:
lines.append(f'{inner}<MainAttribute>true</MainAttribute>')
if attr.get('savedData') is True:
main_saved = False
if attr.get('main') is True and attr.get('type'):
t = str(attr['type'])
main_saved = bool(re.match(r'^(CatalogObject|DocumentObject|ChartOfAccountsObject|ChartOfCalculationTypesObject|ChartOfCharacteristicTypesObject|ExchangePlanObject|BusinessProcessObject|TaskObject)\.', t)) or ('RecordManager.' in t)
if attr.get('savedData') is True or main_saved:
lines.append(f'{inner}<SavedData>true</SavedData>')
if attr.get('fillChecking'):
lines.append(f'{inner}<FillChecking>{attr["fillChecking"]}</FillChecking>')
@@ -2210,8 +2535,9 @@ def emit_commands(lines, cmds, indent):
lines.append(f'{indent}\t<Command name="{cmd["name"]}" id="{cmd_id}">')
inner = f'{indent}\t\t'
if cmd.get('title'):
emit_mltext(lines, inner, 'Title', str(cmd['title']))
cmd_title = cmd.get('title') or title_from_name(str(cmd['name']))
if cmd_title:
emit_mltext(lines, inner, 'Title', str(cmd_title))
if cmd.get('action'):
lines.append(f'{inner}<Action>{cmd["action"]}</Action>')
@@ -2533,6 +2859,131 @@ def main():
with open(json_path, 'r', encoding='utf-8-sig') as f:
defn = json.load(f)
# --- 1b. Pre-pass: synonyms, main attribute inference, heuristics, autoCmdBar extraction ---
def _normalize_synonyms(el):
if not isinstance(el, dict):
return
synonyms = {'commandBar': 'cmdBar', 'autoCommandBar': 'autoCmdBar'}
for src, dst in synonyms.items():
if src in el and dst not in el:
el[dst] = el.pop(src)
if isinstance(el.get('children'), list):
for child in el['children']:
_normalize_synonyms(child)
if isinstance(el.get('columns'), list):
for child in el['columns']:
_normalize_synonyms(child)
def _has_cmd_bar_recursive(el):
if not isinstance(el, dict):
return False
if el.get('cmdBar') is not None:
return True
if isinstance(el.get('children'), list):
for child in el['children']:
if _has_cmd_bar_recursive(child):
return True
if isinstance(el.get('columns'), list):
for child in el['columns']:
if _has_cmd_bar_recursive(child):
return True
return False
def _apply_dlist_table_heuristic(el, list_name, has_main_table):
if not isinstance(el, dict):
return
if el.get('table') is not None and str(el.get('path', '')) == list_name:
if 'tableAutofill' not in el:
el['tableAutofill'] = False
if 'commandBarLocation' not in el:
el['commandBarLocation'] = 'None'
# DefaultPicture доступен только если у DynamicList есть основная таблица
if has_main_table and not el.get('rowPictureDataPath'):
el['rowPictureDataPath'] = f'{list_name}.DefaultPicture'
if isinstance(el.get('children'), list):
for child in el['children']:
_apply_dlist_table_heuristic(child, list_name, has_main_table)
def _is_object_like_type(t):
if not t:
return False
if t == 'DynamicList' or t == 'ConstantsSet':
return True
object_suffixes = (
'CatalogObject', 'DocumentObject', 'DataProcessorObject', 'ReportObject',
'ExternalDataProcessorObject', 'ExternalReportObject', 'BusinessProcessObject',
'TaskObject', 'ChartOfAccountsObject', 'ChartOfCharacteristicTypesObject',
'ChartOfCalculationTypesObject', 'ExchangePlanObject',
)
record_set_prefixes = (
'InformationRegisterRecordSet', 'AccumulationRegisterRecordSet',
'AccountingRegisterRecordSet', 'CalculationRegisterRecordSet',
'InformationRegisterRecordManager',
)
for s in object_suffixes:
if t.startswith(s + '.'):
return True
for s in record_set_prefixes:
if t.startswith(s + '.'):
return True
return False
# 1b.1: Normalize synonyms recursively
if isinstance(defn.get('elements'), list):
for el in defn['elements']:
_normalize_synonyms(el)
# 1b.2: Extract autoCmdBar element from defn['elements']
main_acb_def = None
if isinstance(defn.get('elements'), list):
auto_bars = [el for el in defn['elements'] if isinstance(el, dict) and el.get('autoCmdBar') is not None]
if len(auto_bars) > 1:
print(f"form-compile: more than one autoCmdBar in def.elements (found {len(auto_bars)}); only one allowed.", file=sys.stderr)
sys.exit(1)
if len(auto_bars) == 1:
main_acb_def = auto_bars[0]
defn['elements'] = [el for el in defn['elements'] if el is not main_acb_def]
# 1b.3: Infer main attribute
if isinstance(defn.get('attributes'), list):
has_explicit_main = any(a.get('main') is True for a in defn['attributes'] if isinstance(a, dict))
if not has_explicit_main:
candidates = []
for a in defn['attributes']:
if not isinstance(a, dict):
continue
if 'main' in a and a.get('main') is False:
continue
if _is_object_like_type(str(a.get('type', ''))):
candidates.append(a)
if len(candidates) == 1:
candidates[0]['main'] = True
print(f"[INFO] Inferred main attribute: {candidates[0].get('name')} ({candidates[0].get('type')})")
elif len(candidates) > 1:
names = ', '.join(c.get('name', '') for c in candidates)
print(f"[WARN] Multiple main-attribute candidates: {names}; specify \"main\": true explicitly")
# 1b.4: DynamicList → table heuristic
if isinstance(defn.get('attributes'), list) and isinstance(defn.get('elements'), list):
main_attr = next((a for a in defn['attributes'] if isinstance(a, dict) and a.get('main') is True), None)
if main_attr and str(main_attr.get('type', '')) == 'DynamicList':
settings = main_attr.get('settings') or {}
has_mt = bool(isinstance(settings, dict) and settings.get('mainTable'))
for el in defn['elements']:
_apply_dlist_table_heuristic(el, main_attr.get('name', ''), has_mt)
# 1b.5: Compute main AutoCommandBar Autofill (B3)
def _compute_main_acb_autofill():
if main_acb_def is not None:
if 'autofill' in main_acb_def:
return bool(main_acb_def.get('autofill'))
return True
if isinstance(defn.get('elements'), list):
for el in defn['elements']:
if _has_cmd_bar_recursive(el):
return False
return True
# --- 2. Main compilation ---
_next_id = 0
lines = []
@@ -2548,9 +2999,16 @@ def main():
emit_mltext(lines, '\t', 'Title', str(form_title))
# Properties (skip 'title' — handled above)
if defn.get('properties'):
props_clone = {k: v for k, v in defn['properties'].items() if k != 'title'}
emit_properties(lines, props_clone, '\t')
# When form-level Title is set, default autoTitle=false (≈95% of ERP forms do this;
# otherwise platform appends synonym → "Title: Synonym" double-titles).
props_src = defn.get('properties') or {}
props_clone = OrderedDict()
if form_title and 'autoTitle' not in props_src:
props_clone['autoTitle'] = False
for k, v in props_src.items():
if k != 'title':
props_clone[k] = v
emit_properties(lines, props_clone, '\t')
# CommandSet (excluded commands)
if defn.get('excludedCommands') and len(defn['excludedCommands']) > 0:
@@ -2560,10 +3018,33 @@ def main():
lines.append('\t</CommandSet>')
# AutoCommandBar (always present, id=-1)
lines.append('\t<AutoCommandBar name="\u0424\u043e\u0440\u043c\u0430\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c" id="-1">')
lines.append('\t\t<HorizontalAlign>Right</HorizontalAlign>')
lines.append('\t\t<Autofill>false</Autofill>')
lines.append('\t</AutoCommandBar>')
acb_autofill = _compute_main_acb_autofill()
acb_name = '\u0424\u043e\u0440\u043c\u0430\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c'
acb_halign = None
if main_acb_def is not None:
v = main_acb_def.get('autoCmdBar')
if v:
acb_name = str(v)
if main_acb_def.get('name'):
acb_name = str(main_acb_def['name'])
if main_acb_def.get('horizontalAlign'):
acb_halign = str(main_acb_def['horizontalAlign'])
has_acb_children = bool(main_acb_def and isinstance(main_acb_def.get('children'), list) and len(main_acb_def['children']) > 0)
has_inner = bool(acb_halign) or (not acb_autofill) or has_acb_children
if has_inner:
lines.append(f'\t<AutoCommandBar name="{acb_name}" id="-1">')
if acb_halign:
lines.append(f'\t\t<HorizontalAlign>{acb_halign}</HorizontalAlign>')
if not acb_autofill:
lines.append('\t\t<Autofill>false</Autofill>')
if has_acb_children:
lines.append('\t\t<ChildItems>')
for child in main_acb_def['children']:
emit_element(lines, child, '\t\t\t', in_cmd_bar=True)
lines.append('\t\t</ChildItems>')
lines.append('\t</AutoCommandBar>')
else:
lines.append(f'\t<AutoCommandBar name="{acb_name}" id="-1"/>')
# Events
if defn.get('events'):
+1 -1
View File
@@ -29,7 +29,7 @@ allowed-tools:
## Команда
```powershell
powershell.exe -NoProfile -File .claude/skills/form-edit/scripts/form-edit.ps1 -FormPath "<путь>" -JsonPath "<путь>"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-edit.ps1" -FormPath "<путь>" -JsonPath "<путь>"
```
## JSON формат
@@ -2,6 +2,7 @@
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
[Alias('Path')]
[string]$FormPath,
[Parameter(Mandatory)]
@@ -14,7 +14,7 @@ sys.stderr.reconfigure(encoding="utf-8")
# ── arg parsing ──────────────────────────────────────────────
parser = argparse.ArgumentParser(allow_abbrev=False)
parser.add_argument("-FormPath", required=True)
parser.add_argument("-FormPath", "-Path", required=True)
parser.add_argument("-JsonPath", required=True)
args = parser.parse_args()
+1 -1
View File
@@ -15,7 +15,7 @@ allowed-tools:
## Команда
```powershell
powershell.exe -NoProfile -File .claude/skills/form-info/scripts/form-info.ps1 -FormPath "<путь к Form.xml>"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-info.ps1" -FormPath "<путь к Form.xml>"
```
## Параметры
+64 -1
View File
@@ -1,7 +1,8 @@
# form-info v1.2 — Analyze 1C managed form structure
# form-info v1.3 — Analyze 1C managed form structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory=$true)]
[Alias('Path')]
[string]$FormPath,
[int]$Limit = 150,
[int]$Offset = 0,
@@ -435,6 +436,62 @@ if ($formEvents -and $formEvents.HasChildNodes) {
}
}
# --- Main AutoCommandBar (form's id=-1 panel) ---
function Format-MainAcb($acbNode) {
if (-not $acbNode) { return @() }
$result = @()
$autofillNode = $acbNode.SelectSingleNode("d:Autofill", $ns)
$autofill = $true
if ($autofillNode -and $autofillNode.InnerText -eq "false") { $autofill = $false }
$halignNode = $acbNode.SelectSingleNode("d:HorizontalAlign", $ns)
$flags = @()
$flags += if ($autofill) { "autofill" } else { "no-autofill" }
if ($halignNode) { $flags += "align=$($halignNode.InnerText)" }
$header = "AutoCommandBar [$($flags -join ', ')]"
$childItemsNode = $acbNode.SelectSingleNode("d:ChildItems", $ns)
$buttons = @()
if ($childItemsNode) {
foreach ($btn in $childItemsNode.ChildNodes) {
if ($btn.NodeType -ne "Element") { continue }
if ($skipElements.ContainsKey($btn.LocalName)) { continue }
$bName = $btn.GetAttribute("name")
$cmdNode = $btn.SelectSingleNode("d:CommandName", $ns)
$cmdRef = if ($cmdNode) { $cmdNode.InnerText } else { "" }
$locNode = $btn.SelectSingleNode("d:LocationInCommandBar", $ns)
$locStr = if ($locNode) { " [$($locNode.InnerText)]" } else { "" }
$tag = Get-ElementTag $btn
if ($cmdRef) {
$buttons += " $tag $bName -> $cmdRef$locStr"
} else {
$buttons += " $tag $bName$locStr"
}
}
}
if ($buttons.Count -eq 0 -and $autofill -and -not $halignNode) {
# Default empty panel — terse one-liner
return @("AutoCommandBar [autofill]")
}
$result += $header
$result += $buttons
return $result
}
# Determine position from CommandBarLocation form property
$cbLocNode = $root.SelectSingleNode("d:CommandBarLocation", $ns)
$cbLoc = if ($cbLocNode) { $cbLocNode.InnerText } else { "Auto" }
$mainAcbNode = $root.SelectSingleNode("d:AutoCommandBar", $ns)
$acbLines = @()
if ($cbLoc -ne "None" -and $mainAcbNode) {
$acbLines = Format-MainAcb $mainAcbNode
}
# AutoCommandBar above Elements (Auto/Top)
if ($acbLines.Count -gt 0 -and ($cbLoc -eq "Auto" -or $cbLoc -eq "Top")) {
$lines += ""
$lines += $acbLines
}
# --- Element tree ---
$childItems = $root.SelectSingleNode("d:ChildItems", $ns)
@@ -445,6 +502,12 @@ if ($childItems) {
$lines += $treeLines.ToArray()
}
# AutoCommandBar below Elements (Bottom)
if ($acbLines.Count -gt 0 -and $cbLoc -eq "Bottom") {
$lines += ""
$lines += $acbLines
}
# --- Attributes ---
$attrsNode = $root.SelectSingleNode("d:Attributes", $ns)
+50 -2
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# form-info v1.2 — Analyze 1C managed form structure
# form-info v1.3 — Analyze 1C managed form structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -342,7 +342,7 @@ def main():
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
parser = argparse.ArgumentParser(description="Analyze 1C managed form structure", allow_abbrev=False)
parser.add_argument("-FormPath", required=True, help="Path to Form.xml")
parser.add_argument("-FormPath", "-Path", required=True, help="Path to Form.xml")
parser.add_argument("-Limit", type=int, default=150, help="Max lines to show")
parser.add_argument("-Offset", type=int, default=0, help="Line offset for pagination")
parser.add_argument("-Expand", default="", help="Expand collapsed section by name, or * for all")
@@ -484,6 +484,50 @@ def main():
ct_str = f"[{ct}]" if ct else ""
lines.append(f" {e_name}{ct_str} -> {e_handler}")
# --- Main AutoCommandBar (form's id=-1 panel) ---
def format_main_acb(acb_node):
if acb_node is None:
return []
autofill_node = acb_node.find("d:Autofill", NSMAP)
autofill = not (autofill_node is not None and autofill_node.text == "false")
halign_node = acb_node.find("d:HorizontalAlign", NSMAP)
flags = ["autofill" if autofill else "no-autofill"]
if halign_node is not None and halign_node.text:
flags.append(f"align={halign_node.text}")
ci_node = acb_node.find("d:ChildItems", NSMAP)
buttons = []
if ci_node is not None:
for btn in ci_node:
if not isinstance(btn.tag, str):
continue
ln = etree.QName(btn).localname
if ln in SKIP_ELEMENTS:
continue
b_name = btn.get("name", "")
cmd_node = btn.find("d:CommandName", NSMAP)
cmd_ref = cmd_node.text if cmd_node is not None and cmd_node.text else ""
loc_node = btn.find("d:LocationInCommandBar", NSMAP)
loc_str = f" [{loc_node.text}]" if loc_node is not None and loc_node.text else ""
tag = get_element_tag(btn)
if cmd_ref:
buttons.append(f" {tag} {b_name} -> {cmd_ref}{loc_str}")
else:
buttons.append(f" {tag} {b_name}{loc_str}")
if not buttons and autofill and halign_node is None:
return ["AutoCommandBar [autofill]"]
return [f"AutoCommandBar [{', '.join(flags)}]"] + buttons
cb_loc_node = root.find("d:CommandBarLocation", NSMAP)
cb_loc = cb_loc_node.text if cb_loc_node is not None and cb_loc_node.text else "Auto"
main_acb_node = root.find("d:AutoCommandBar", NSMAP)
acb_lines = []
if cb_loc != "None" and main_acb_node is not None:
acb_lines = format_main_acb(main_acb_node)
if acb_lines and cb_loc in ("Auto", "Top"):
lines.append("")
lines.extend(acb_lines)
# --- Element tree ---
tree_state = {"has_collapsed": False}
child_items = root.find("d:ChildItems", NSMAP)
@@ -494,6 +538,10 @@ def main():
build_tree(child_items, " ", tree_lines, expand, tree_state)
lines.extend(tree_lines)
if acb_lines and cb_loc == "Bottom":
lines.append("")
lines.extend(acb_lines)
# --- Attributes ---
attrs_node = root.find("d:Attributes", NSMAP)
if attrs_node is not None:
+11 -11
View File
@@ -41,9 +41,9 @@ allowed-tools: []
├─ Информационные надписи (label, hyperlink)
Рабочая область
├─ Таблица данных или Pages с вкладками
Кнопки действий
├─ Выполнить / Применить (defaultButton)
─ Закрыть (stdCommand: Close)
Главная АКП формы (autoCmdBar)
├─ Выполнить / Применить (defaultButton: true)
─ Закрыть (stdCommand: Close)
```
**События:** OnCreateAtServer, OnOpen, NotificationProcessing
@@ -90,12 +90,12 @@ allowed-tools: []
├─ Шаг1: описание + параметры
├─ Шаг2: основная работа
└─ Шаг3: результат
Кнопки (horizontal)
├─ Назад (command), Далее (command, defaultButton), Выполнить (command)
Главная АКП формы (autoCmdBar)
├─ Назад, Далее (defaultButton: true), Выполнить
└─ Закрыть (stdCommand: Close)
```
**Свойства:** windowOpeningMode=LockOwnerWindow, commandBarLocation=None
**Свойства:** windowOpeningMode=LockOwnerWindow
---
@@ -111,7 +111,7 @@ allowed-tools: []
| Номер+Дата | `ГруппаНомерДата` | horizontal |
| Подвал | `ГруппаПодвал` | vertical |
| Итоги | `ГруппаИтоги` | horizontal |
| Кнопки | `ГруппаКнопок` | horizontal |
| Главная АКП формы | `ФормаКоманднаяПанель` | autoCmdBar |
| Страницы | `ГруппаСтраницы` / `Страницы` | pages |
| Предупреждение | `ГруппаПредупреждение` | horizontal, visible:false |
| Доп. секция | `ГруппаДополнительно` / `ГруппаПрочее` | vertical, collapse |
@@ -150,7 +150,7 @@ allowed-tools: []
1. **Порядок чтения.** Сверху вниз, слева направо. Самое важное — вверху.
2. **Двухколоночная шапка.** Основные реквизиты слева (контрагент, склад), организационные справа (организация, подразделение).
3. **Кнопки действий внизу.** Главная кнопка — `defaultButton: true`. Закрыть — всегда последняя.
3. **Кнопки действий — на главной АКП формы** (`autoCmdBar`), не в отдельной группе на форме. Главная кнопка — `defaultButton: true`. Закрыть — всегда последняя.
4. **Таблицы — основная область.** Табличные части занимают большую часть формы, обычно на Pages.
5. **Итоги рядом с таблицей.** В подвале, горизонтальная группа, все поля readOnly.
6. **Фильтры — отдельная зона.** Над списком, alwaysHorizontal, пара «флажок + поле» на каждый фильтр.
@@ -166,8 +166,8 @@ allowed-tools: []
Для необязательных секций (подписи, дополнительно, прочее):
```json
{ "group": "vertical", "name": "ГруппаПодписи", "title": "Подписи",
"behavior": "Collapsible", "collapsed": true, "children": [...] }
{ "group": "collapsible", "name": "ГруппаПодписи", "title": "Подписи",
"collapsed": true, "children": [...] }
```
### Баннер-предупреждение
@@ -233,7 +233,7 @@ allowed-tools: []
{ "input": "ДанныеКоличество", "path": "Объект.Данные.Количество", "on": ["OnChange"] },
{ "input": "ДанныеСумма", "path": "Объект.Данные.Сумма", "readOnly": true }
]},
{ "group": "horizontal", "name": "ГруппаКнопок", "children": [
{ "autoCmdBar": "ФормаКоманднаяПанель", "children": [
{ "button": "Загрузить", "command": "Загрузить", "title": "Загрузить из файла", "defaultButton": true },
{ "button": "Очистить", "command": "Очистить", "title": "Очистить таблицу" },
{ "button": "Закрыть", "stdCommand": "Close" }
+1 -1
View File
@@ -31,7 +31,7 @@ allowed-tools:
## Команда
```powershell
powershell.exe -NoProfile -File .claude/skills/form-remove/scripts/remove-form.ps1 -ObjectName "<ObjectName>" -FormName "<FormName>" [-SrcDir "<SrcDir>"]
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/remove-form.ps1" -ObjectName "<ObjectName>" -FormName "<FormName>" [-SrcDir "<SrcDir>"]
```
## Что удаляется
@@ -1,4 +1,4 @@
# form-remove v1.1 — Remove form from 1C object
# form-remove v1.2 — Remove form from 1C object
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -12,6 +12,8 @@ param(
)
$ErrorActionPreference = "Stop"
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::InputEncoding = [System.Text.Encoding]::UTF8
# --- Проверки ---
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# remove-form v1.0 — Remove form from 1C object
# remove-form v1.1 — Remove form from 1C object
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
+2 -2
View File
@@ -23,7 +23,7 @@ allowed-tools:
## Команда
```powershell
powershell.exe -NoProfile -File .claude/skills/form-validate/scripts/form-validate.ps1 -FormPath "Catalogs/Номенклатура/Forms/ФормаЭлемента"
powershell.exe -NoProfile -File .claude/skills/form-validate/scripts/form-validate.ps1 -FormPath "src/МояОбработка/Forms/Форма/Ext/Form.xml"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-validate.ps1" -FormPath "Catalogs/Номенклатура/Forms/ФормаЭлемента"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-validate.ps1" -FormPath "src/МояОбработка/Forms/Форма/Ext/Form.xml"
```
@@ -2,6 +2,7 @@
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
[Alias('Path')]
[string]$FormPath,
[switch]$Detailed,
@@ -57,7 +57,7 @@ def main():
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
parser = argparse.ArgumentParser(description="Validate 1C managed form", allow_abbrev=False)
parser.add_argument("-FormPath", required=True)
parser.add_argument("-FormPath", "-Path", required=True)
parser.add_argument("-Detailed", action="store_true")
parser.add_argument("-MaxErrors", type=int, default=30)
args = parser.parse_args()
+1 -1
View File
@@ -30,7 +30,7 @@ allowed-tools:
## Команда
```powershell
powershell.exe -NoProfile -File .claude/skills/help-add/scripts/add-help.ps1 -ObjectName "<ObjectName>" [-Lang "<Lang>"] [-SrcDir "<SrcDir>"]
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/add-help.ps1" -ObjectName "<ObjectName>" [-Lang "<Lang>"] [-SrcDir "<SrcDir>"]
```
## Что делает скрипт
+3 -1
View File
@@ -1,4 +1,4 @@
# help-add v1.3 — Add built-in help to 1C object
# help-add v1.4 — Add built-in help to 1C object
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -10,6 +10,8 @@ param(
)
$ErrorActionPreference = "Stop"
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::InputEncoding = [System.Text.Encoding]::UTF8
# --- Detect format version ---
+1 -1
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# add-help v1.3 — Add built-in help to 1C object
# add-help v1.4 — Add built-in help to 1C object
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
+2 -2
View File
@@ -1,6 +1,6 @@
---
name: img-grid
description: Наложить пронумерованную сетку на изображение для определения пропорций колонок
description: Наложить пронумерованную сетку на изображение. Используй при анализе скриншота макета или печатной формы — измерить пропорции колонок перед генерацией табличного документа
argument-hint: <ImagePath> [-c COLS]
allowed-tools:
- Bash
@@ -29,7 +29,7 @@ allowed-tools:
## Команда
```bash
python .claude/skills/img-grid/scripts/overlay-grid.py "<ImagePath>" [-c 50] [-o "<OutputPath>"]
python "${CLAUDE_SKILL_DIR}/scripts/overlay-grid.py" "<ImagePath>" [-c 50] [-o "<OutputPath>"]
```
Требуется Python 3 с библиотекой Pillow (`pip install Pillow`).
+2 -2
View File
@@ -29,13 +29,13 @@ allowed-tools:
### Inline mode
```powershell
powershell.exe -NoProfile -File '.claude/skills/interface-edit/scripts/interface-edit.ps1' -CIPath '<path>' -Operation hide -Value '<cmd>'
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/interface-edit.ps1" -CIPath '<path>' -Operation hide -Value '<cmd>'
```
### JSON mode
```powershell
powershell.exe -NoProfile -File '.claude/skills/interface-edit/scripts/interface-edit.ps1' -CIPath '<path>' -DefinitionFile '<json>'
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/interface-edit.ps1" -CIPath '<path>' -DefinitionFile '<json>'
```
## Операции
@@ -1,7 +1,7 @@
# interface-edit v1.3 — Edit 1C CommandInterface.xml
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)][string]$CIPath,
[Parameter(Mandatory)][Alias('Path')][string]$CIPath,
[string]$DefinitionFile,
[ValidateSet("hide","show","place","order","subsystem-order","group-order")]
[string]$Operation,
@@ -182,7 +182,7 @@ def main():
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
parser = argparse.ArgumentParser(description="Edit 1C CommandInterface.xml", allow_abbrev=False)
parser.add_argument("-CIPath", required=True)
parser.add_argument("-CIPath", "-Path", required=True)
parser.add_argument("-DefinitionFile", default=None)
parser.add_argument("-Operation", default=None, choices=["hide", "show", "place", "order", "subsystem-order", "group-order"])
parser.add_argument("-Value", default=None)
@@ -504,7 +504,7 @@ def main():
if os.path.isfile(validate_script):
print()
print("--- Running interface-validate ---")
subprocess.run([sys.executable, validate_script, "-CIPath", resolved_path])
subprocess.run([sys.executable, validate_script, "-CIPath", "-Path", resolved_path])
# --- Summary ---
print()
+2 -2
View File
@@ -24,6 +24,6 @@ allowed-tools:
## Команда
```powershell
powershell.exe -NoProfile -File ".claude/skills/interface-validate/scripts/interface-validate.ps1" -CIPath "Subsystems/Продажи"
powershell.exe -NoProfile -File ".claude/skills/interface-validate/scripts/interface-validate.ps1" -CIPath "Subsystems/Продажи/Ext/CommandInterface.xml"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/interface-validate.ps1" -CIPath "Subsystems/Продажи"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/interface-validate.ps1" -CIPath "Subsystems/Продажи/Ext/CommandInterface.xml"
```
@@ -1,7 +1,7 @@
# interface-validate v1.1 — Validate 1C CommandInterface.xml structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)][string]$CIPath,
[Parameter(Mandatory)][Alias('Path')][string]$CIPath,
[switch]$Detailed,
[int]$MaxErrors = 30,
[string]$OutFile
@@ -79,7 +79,7 @@ def main():
parser = argparse.ArgumentParser(
description='Validate 1C CommandInterface.xml structure', allow_abbrev=False
)
parser.add_argument('-CIPath', dest='CIPath', required=True)
parser.add_argument('-CIPath', '-Path', dest='CIPath', 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='')
+2 -2
View File
@@ -1,6 +1,6 @@
---
name: meta-compile
description: Создать объект метаданных 1С. Используй когда пользователь просит создать или добавить справочник, документ, регистр, перечисление, константу, общий модуль, обработку, отчёт и др.
description: Создать объект метаданных 1С. Используй когда нужно создать или добавить справочник, документ, регистр, перечисление, константу, общий модуль, обработку, отчёт и др.
argument-hint: <JsonPath> <OutputDir>
allowed-tools:
- Bash
@@ -23,7 +23,7 @@ allowed-tools:
## Команда
```powershell
powershell.exe -NoProfile -File .claude/skills/meta-compile/scripts/meta-compile.ps1 -JsonPath "<json>" -OutputDir "<ConfigDir>"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/meta-compile.ps1" -JsonPath "<json>" -OutputDir "<ConfigDir>"
```
| Параметр | Описание |
@@ -18,7 +18,7 @@
| `checkUnique` | `false` | CheckUnique |
| `defaultPresentation` | `AsDescription` | DefaultPresentation |
| `subordinationUse` | `ToItems` | SubordinationUse |
| `quickChoice` | `true` | QuickChoice |
| `quickChoice` | `false` | QuickChoice |
| `choiceMode` | `BothWays` | ChoiceMode |
| `owners` | `[]` | Owners |
| `attributes` | `[]` | → Attribute в ChildObjects |
@@ -1,4 +1,4 @@
# meta-compile v1.10 — Compile 1C metadata object from JSON
# meta-compile v1.11 — Compile 1C metadata object from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -1129,7 +1129,7 @@ function Emit-CatalogProperties {
X "$i<Characteristics/>"
X "$i<PredefinedDataUpdate>Auto</PredefinedDataUpdate>"
X "$i<EditType>InDialog</EditType>"
$quickChoice = if ($def.quickChoice -eq $false) { "false" } else { "true" }
$quickChoice = if ($def.quickChoice -eq $true) { "true" } else { "false" }
$choiceMode = Get-EnumProp "ChoiceMode" "choiceMode" "BothWays"
X "$i<QuickChoice>$quickChoice</QuickChoice>"
X "$i<ChoiceMode>$choiceMode</ChoiceMode>"
@@ -1292,7 +1292,8 @@ function Emit-EnumProperties {
Emit-StandardAttributes $i "Enum"
X "$i<Characteristics/>"
X "$i<QuickChoice>true</QuickChoice>"
$quickChoice = if ($def.quickChoice -eq $false) { "false" } else { "true" }
X "$i<QuickChoice>$quickChoice</QuickChoice>"
X "$i<ChoiceMode>BothWays</ChoiceMode>"
X "$i<DefaultListForm/>"
X "$i<DefaultChoiceForm/>"
@@ -1663,7 +1664,8 @@ function Emit-ExchangePlanProperties {
X "$i<IncludeConfigurationExtensions>$includeExt</IncludeConfigurationExtensions>"
X "$i<BasedOn/>"
X "$i<QuickChoice>true</QuickChoice>"
$quickChoice = if ($def.quickChoice -eq $true) { "true" } else { "false" }
X "$i<QuickChoice>$quickChoice</QuickChoice>"
X "$i<ChoiceMode>BothWays</ChoiceMode>"
X "$i<InputByString>"
X "$i`t<xr:Field>ExchangePlan.$objName.StandardAttribute.Description</xr:Field>"
@@ -1764,7 +1766,8 @@ function Emit-ChartOfCharacteristicTypesProperties {
X "$i<Characteristics/>"
X "$i<PredefinedDataUpdate>Auto</PredefinedDataUpdate>"
X "$i<EditType>InDialog</EditType>"
X "$i<QuickChoice>true</QuickChoice>"
$quickChoice = if ($def.quickChoice -eq $true) { "true" } else { "false" }
X "$i<QuickChoice>$quickChoice</QuickChoice>"
X "$i<ChoiceMode>BothWays</ChoiceMode>"
X "$i<InputByString>"
X "$i`t<xr:Field>ChartOfCharacteristicTypes.$objName.StandardAttribute.Description</xr:Field>"
@@ -1905,7 +1908,8 @@ function Emit-ChartOfAccountsProperties {
X "$i<Characteristics/>"
X "$i<PredefinedDataUpdate>Auto</PredefinedDataUpdate>"
X "$i<QuickChoice>true</QuickChoice>"
$quickChoice = if ($def.quickChoice -eq $true) { "true" } else { "false" }
X "$i<QuickChoice>$quickChoice</QuickChoice>"
X "$i<ChoiceMode>BothWays</ChoiceMode>"
X "$i<InputByString>"
X "$i`t<xr:Field>ChartOfAccounts.$objName.StandardAttribute.Description</xr:Field>"
@@ -2021,7 +2025,8 @@ function Emit-ChartOfCalculationTypesProperties {
X "$i<Characteristics/>"
X "$i<PredefinedDataUpdate>Auto</PredefinedDataUpdate>"
X "$i<EditType>InDialog</EditType>"
X "$i<QuickChoice>true</QuickChoice>"
$quickChoice = if ($def.quickChoice -eq $true) { "true" } else { "false" }
X "$i<QuickChoice>$quickChoice</QuickChoice>"
X "$i<ChoiceMode>BothWays</ChoiceMode>"
X "$i<InputByString>"
X "$i`t<xr:Field>ChartOfCalculationTypes.$objName.StandardAttribute.Description</xr:Field>"
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# meta-compile v1.10 — Compile 1C metadata object from JSON
# meta-compile v1.11 — Compile 1C metadata object from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -1024,7 +1024,7 @@ def emit_catalog_properties(indent):
X(f'{i}<Characteristics/>')
X(f'{i}<PredefinedDataUpdate>Auto</PredefinedDataUpdate>')
X(f'{i}<EditType>InDialog</EditType>')
quick_choice = 'false' if defn.get('quickChoice') is False else 'true'
quick_choice = 'true' if defn.get('quickChoice') is True else 'false'
choice_mode = get_enum_prop('ChoiceMode', 'choiceMode', 'BothWays')
X(f'{i}<QuickChoice>{quick_choice}</QuickChoice>')
X(f'{i}<ChoiceMode>{choice_mode}</ChoiceMode>')
@@ -1157,7 +1157,8 @@ def emit_enum_properties(indent):
X(f'{i}<UseStandardCommands>false</UseStandardCommands>')
emit_standard_attributes(i, 'Enum')
X(f'{i}<Characteristics/>')
X(f'{i}<QuickChoice>true</QuickChoice>')
quick_choice = 'false' if defn.get('quickChoice') is False else 'true'
X(f'{i}<QuickChoice>{quick_choice}</QuickChoice>')
X(f'{i}<ChoiceMode>BothWays</ChoiceMode>')
X(f'{i}<DefaultListForm/>')
X(f'{i}<DefaultChoiceForm/>')
@@ -1471,7 +1472,8 @@ def emit_exchange_plan_properties(indent):
X(f'{i}<DistributedInfoBase>{distributed}</DistributedInfoBase>')
X(f'{i}<IncludeConfigurationExtensions>{include_ext}</IncludeConfigurationExtensions>')
X(f'{i}<BasedOn/>')
X(f'{i}<QuickChoice>true</QuickChoice>')
quick_choice = 'true' if defn.get('quickChoice') is True else 'false'
X(f'{i}<QuickChoice>{quick_choice}</QuickChoice>')
X(f'{i}<ChoiceMode>BothWays</ChoiceMode>')
X(f'{i}<InputByString>')
X(f'{i}\t<xr:Field>ExchangePlan.{obj_name}.StandardAttribute.Description</xr:Field>')
@@ -1557,7 +1559,8 @@ def emit_chart_of_characteristic_types_properties(indent):
X(f'{i}<Characteristics/>')
X(f'{i}<PredefinedDataUpdate>Auto</PredefinedDataUpdate>')
X(f'{i}<EditType>InDialog</EditType>')
X(f'{i}<QuickChoice>true</QuickChoice>')
quick_choice = 'true' if defn.get('quickChoice') is True else 'false'
X(f'{i}<QuickChoice>{quick_choice}</QuickChoice>')
X(f'{i}<ChoiceMode>BothWays</ChoiceMode>')
X(f'{i}<InputByString>')
X(f'{i}\t<xr:Field>ChartOfCharacteristicTypes.{obj_name}.StandardAttribute.Description</xr:Field>')
@@ -1673,7 +1676,8 @@ def emit_chart_of_accounts_properties(indent):
X(f'{i}</StandardTabularSections>')
X(f'{i}<Characteristics/>')
X(f'{i}<PredefinedDataUpdate>Auto</PredefinedDataUpdate>')
X(f'{i}<QuickChoice>true</QuickChoice>')
quick_choice = 'true' if defn.get('quickChoice') is True else 'false'
X(f'{i}<QuickChoice>{quick_choice}</QuickChoice>')
X(f'{i}<ChoiceMode>BothWays</ChoiceMode>')
X(f'{i}<InputByString>')
X(f'{i}\t<xr:Field>ChartOfAccounts.{obj_name}.StandardAttribute.Description</xr:Field>')
@@ -1764,7 +1768,8 @@ def emit_chart_of_calculation_types_properties(indent):
X(f'{i}<Characteristics/>')
X(f'{i}<PredefinedDataUpdate>Auto</PredefinedDataUpdate>')
X(f'{i}<EditType>InDialog</EditType>')
X(f'{i}<QuickChoice>true</QuickChoice>')
quick_choice = 'true' if defn.get('quickChoice') is True else 'false'
X(f'{i}<QuickChoice>{quick_choice}</QuickChoice>')
X(f'{i}<ChoiceMode>BothWays</ChoiceMode>')
X(f'{i}<InputByString>')
X(f'{i}\t<xr:Field>ChartOfCalculationTypes.{obj_name}.StandardAttribute.Description</xr:Field>')
+2 -2
View File
@@ -18,13 +18,13 @@ allowed-tools:
### Inline mode (простые операции)
```powershell
powershell.exe -NoProfile -File .claude/skills/meta-edit/scripts/meta-edit.ps1 -ObjectPath "<path>" -Operation <op> -Value "<val>"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/meta-edit.ps1" -ObjectPath "<path>" -Operation <op> -Value "<val>"
```
### JSON mode (сложные/комбинированные)
```powershell
powershell.exe -NoProfile -File .claude/skills/meta-edit/scripts/meta-edit.ps1 -DefinitionFile "<json>" -ObjectPath "<path>"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/meta-edit.ps1" -DefinitionFile "<json>" -ObjectPath "<path>"
```
| Параметр | Описание |
@@ -4,6 +4,7 @@ param(
[string]$DefinitionFile,
[Parameter(Mandatory)]
[Alias('Path')]
[string]$ObjectPath,
# Inline mode (alternative to DefinitionFile)
@@ -2121,7 +2121,7 @@ def main():
parser = argparse.ArgumentParser(description="Edit existing 1C metadata object XML", allow_abbrev=False)
parser.add_argument("-DefinitionFile", default=None, help="JSON definition file")
parser.add_argument("-ObjectPath", required=True, help="Path to object XML or directory")
parser.add_argument("-ObjectPath", "-Path", required=True, help="Path to object XML or directory")
parser.add_argument("-Operation", default=None, choices=valid_operations, help="Inline operation")
parser.add_argument("-Value", default=None, help="Inline value")
parser.add_argument("-NoValidate", action="store_true", help="Skip auto-validation")
@@ -2257,7 +2257,7 @@ def main():
print()
print("--- Running meta-validate ---")
python_exe = sys.executable
subprocess.run([python_exe, validate_script, "-ObjectPath", resolved_path])
subprocess.run([python_exe, validate_script, "-ObjectPath", "-Path", resolved_path])
else:
print()
print(f"[SKIP] meta-validate not found at: {validate_script}")
+1 -1
View File
@@ -23,7 +23,7 @@ allowed-tools:
| `OutFile` | Записать результат в файл (UTF-8 BOM) |
```powershell
powershell.exe -NoProfile -File .claude/skills/meta-info/scripts/meta-info.ps1 -ObjectPath "<путь>"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/meta-info.ps1" -ObjectPath "<путь>"
```
## Три режима
@@ -1,7 +1,7 @@
# meta-info v1.1 — Compact summary of 1C metadata object
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory=$true)][string]$ObjectPath,
[Parameter(Mandatory=$true)][Alias('Path')][string]$ObjectPath,
[ValidateSet("overview","brief","full")]
[string]$Mode = "overview",
[string]$Name,
@@ -13,7 +13,7 @@ sys.stderr.reconfigure(encoding="utf-8")
# ── arg parsing ──────────────────────────────────────────────
parser = argparse.ArgumentParser(allow_abbrev=False)
parser.add_argument("-ObjectPath", required=True)
parser.add_argument("-ObjectPath", "-Path", required=True)
parser.add_argument("-Mode", choices=["overview", "brief", "full"], default="overview")
parser.add_argument("-Name", default="")
parser.add_argument("-Limit", type=int, default=150)
+2 -2
View File
@@ -1,6 +1,6 @@
---
name: meta-remove
description: Удалить объект метаданных из конфигурации 1С. Используй когда пользователь просит удалить, убрать объект из конфигурации
description: Удалить объект метаданных из конфигурации 1С. Используй когда нужно удалить, убрать объект из конфигурации
argument-hint: <ConfigDir> -Object <Type.Name>
allowed-tools:
- Bash
@@ -32,7 +32,7 @@ allowed-tools:
## Команда
```powershell
powershell.exe -NoProfile -File .claude/skills/meta-remove/scripts/meta-remove.ps1 -ConfigDir "<путь>" -Object "Catalog.Товары"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/meta-remove.ps1" -ConfigDir "<путь>" -Object "Catalog.Товары"
```
## Поддерживаемые типы
+2 -2
View File
@@ -24,6 +24,6 @@ allowed-tools:
## Команда
```powershell
powershell.exe -NoProfile -File .claude/skills/meta-validate/scripts/meta-validate.ps1 -ObjectPath "Catalogs/Номенклатура/Номенклатура.xml"
powershell.exe -NoProfile -File .claude/skills/meta-validate/scripts/meta-validate.ps1 -ObjectPath "Catalogs/Банки|Documents/Заказ"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/meta-validate.ps1" -ObjectPath "Catalogs/Номенклатура/Номенклатура.xml"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/meta-validate.ps1" -ObjectPath "Catalogs/Банки|Documents/Заказ"
```
@@ -2,6 +2,7 @@
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
[Alias('Path')]
[string]$ObjectPath,
[switch]$Detailed,
@@ -14,7 +14,7 @@ sys.stderr.reconfigure(encoding="utf-8")
# ── arg parsing ──────────────────────────────────────────────
parser = argparse.ArgumentParser(allow_abbrev=False)
parser.add_argument("-ObjectPath", required=True)
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="")
@@ -31,7 +31,7 @@ if len(path_list) > 1:
batch_ok = 0
batch_fail = 0
for single_path in path_list:
cmd = [sys.executable, __file__, "-ObjectPath", single_path, "-MaxErrors", str(max_errors)]
cmd = [sys.executable, __file__, "-ObjectPath", "-Path", single_path, "-MaxErrors", str(max_errors)]
if detailed:
cmd.append("-Detailed")
if out_file:
+1 -1
View File
@@ -29,7 +29,7 @@ allowed-tools:
## Команда
```powershell
powershell.exe -NoProfile -File .claude/skills/mxl-compile/scripts/mxl-compile.ps1 -JsonPath "<путь>.json" -OutputPath "<путь>/Template.xml"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/mxl-compile.ps1" -JsonPath "<путь>.json" -OutputPath "<путь>/Template.xml"
```
## Рабочий процесс
+1 -1
View File
@@ -29,7 +29,7 @@ allowed-tools:
## Команда
```powershell
powershell.exe -NoProfile -File .claude/skills/mxl-decompile/scripts/mxl-decompile.ps1 -TemplatePath "<путь>/Template.xml" [-OutputPath "<путь>.json"]
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/mxl-decompile.ps1" -TemplatePath "<путь>/Template.xml" [-OutputPath "<путь>.json"]
```
## Рабочий процесс
@@ -2,6 +2,7 @@
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
[Alias('Path')]
[string]$TemplatePath,
[string]$OutputPath
@@ -47,7 +47,7 @@ def main():
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
parser = argparse.ArgumentParser(description="Decompile 1C spreadsheet to JSON", allow_abbrev=False)
parser.add_argument("-TemplatePath", required=True, help="Path to Template.xml")
parser.add_argument("-TemplatePath", "-Path", required=True, help="Path to Template.xml")
parser.add_argument("-OutputPath", default=None, help="Output JSON path (stdout if omitted)")
args = parser.parse_args()
+2 -2
View File
@@ -38,12 +38,12 @@ allowed-tools:
## Команда
```powershell
powershell.exe -NoProfile -File .claude/skills/mxl-info/scripts/mxl-info.ps1 -TemplatePath "<путь>"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/mxl-info.ps1" -TemplatePath "<путь>"
```
Или по имени обработки/макета:
```powershell
powershell.exe -NoProfile -File .claude/skills/mxl-info/scripts/mxl-info.ps1 -ProcessorName "<Имя>" -TemplateName "<Макет>" [-SrcDir "<каталог>"]
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/mxl-info.ps1" -ProcessorName "<Имя>" -TemplateName "<Макет>" [-SrcDir "<каталог>"]
```
Дополнительные флаги:
@@ -1,6 +1,7 @@
# mxl-info v1.0 — Analyze 1C spreadsheet structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Alias('Path')]
[string]$TemplatePath,
[string]$ProcessorName,
[string]$TemplateName,

Some files were not shown because too many files have changed in this diff Show More