mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-07-17 16:25:16 +03:00
Compare commits
116 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7fa279c354 | |||
| 28a2a34c84 | |||
| f0f1e88aaa | |||
| e7cbf306a0 | |||
| 610720334b | |||
| 5090deb5bc | |||
| 8b0bcf0194 | |||
| 529a5cacae | |||
| 8b0f55f1cc | |||
| 54cbc69a59 | |||
| ac3047cf55 | |||
| 5da154adea | |||
| f4748d76af | |||
| b992cd11c5 | |||
| fc76407877 | |||
| a55195ab66 | |||
| 1eff62de42 | |||
| eb87be5c04 | |||
| 43ed9ba142 | |||
| 588382cec1 | |||
| e0197683e1 | |||
| 96dad75b2f | |||
| 5c734202b6 | |||
| a92bce05fb | |||
| b8ebbf6a6f | |||
| 43ba6ce16c | |||
| 51e37f9874 | |||
| 62e864e474 | |||
| ddebd7b6df | |||
| 3d16e35e80 | |||
| 56822c4533 | |||
| 32bf9c1a3f | |||
| c94f86a9cd | |||
| 8b5fed98e0 | |||
| 9e677cfc61 | |||
| 211a4726d6 | |||
| 91b39b758b | |||
| 4af69f1600 | |||
| 8c7c442705 | |||
| c541d51f33 | |||
| a650325baf | |||
| 6c19846051 | |||
| eef4f4bcea | |||
| 2c553fee98 | |||
| 95e4674825 | |||
| 9751840cc8 | |||
| f257bb428c | |||
| 71e3691cf1 | |||
| 1af318325d | |||
| 986480748e | |||
| 71309d2bc2 | |||
| e1f81a0cde | |||
| e8cb5440d8 | |||
| c496047c6c | |||
| df9541470c | |||
| 27ecfb707d | |||
| 35bec4a2a1 | |||
| e635a5be52 | |||
| dfb258dd3f | |||
| 1866c1335a | |||
| 4813ec5cf2 | |||
| 06ad2f8f99 | |||
| c8ac191a01 | |||
| c9d83b1c92 | |||
| b1a7e414d0 | |||
| 7736ad68a0 | |||
| 501bb58ddb | |||
| cd3a242b12 | |||
| 7561faf736 | |||
| 2849087fd9 | |||
| 105171cdc2 | |||
| c9cd0d62ab | |||
| c1a0a54971 | |||
| 927c0827f3 | |||
| 56cd18a6b4 | |||
| 3ac1d425cd | |||
| 3c596f4550 | |||
| 36d29a51a9 | |||
| 11e961c816 | |||
| 05ca810461 | |||
| a0407b74dc | |||
| 3aad254399 | |||
| 07753921be | |||
| ba0c71fa45 | |||
| 33c9fdade0 | |||
| 1c1fe7b2d9 | |||
| 0bd2587e74 | |||
| 6f17b1c2f6 | |||
| 36ad686316 | |||
| 66e37fb8cc | |||
| 99c77e1dde | |||
| 8d6612027f | |||
| c3b67a18cb | |||
| 4d1b66638c | |||
| 363a9f34f2 | |||
| 4f8ce7b747 | |||
| fc48d68ed1 | |||
| 3e34ec0bdd | |||
| fff2e83960 | |||
| 1ff209849f | |||
| 1a8415283e | |||
| db1e78a534 | |||
| a828f1847f | |||
| 3e8159b591 | |||
| 57bb964c1e | |||
| 41c4b6b1f7 | |||
| ffb0ee740d | |||
| f5e487096f | |||
| 6d5c1a0b19 | |||
| b322c02fdb | |||
| 61ef7ac891 | |||
| ba19b4111d | |||
| ded11437c5 | |||
| 5eda7f8eb3 | |||
| f39a0d9c5e | |||
| 2347859bdd |
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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 недоступен."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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/"
|
||||
}
|
||||
@@ -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'
|
||||
```
|
||||
|
||||
## Операции
|
||||
|
||||
@@ -23,7 +23,7 @@ allowed-tools:
|
||||
| `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 "<путь>"
|
||||
```
|
||||
|
||||
## Три режима
|
||||
|
||||
@@ -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 "МояКонфигурация"
|
||||
```
|
||||
|
||||
## Примеры
|
||||
|
||||
@@ -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"
|
||||
```
|
||||
|
||||
@@ -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.Контрагенты"
|
||||
```
|
||||
|
||||
## Примеры
|
||||
|
||||
@@ -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 — обзор расширения
|
||||
|
||||
@@ -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 "МоёРасширение"
|
||||
```
|
||||
|
||||
## Примеры
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
## Примеры
|
||||
|
||||
@@ -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"
|
||||
```
|
||||
|
||||
@@ -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 "Новая база"
|
||||
```
|
||||
|
||||
@@ -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 "МоёРасширение"
|
||||
```
|
||||
|
||||
@@ -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 "МоёРасширение"
|
||||
```
|
||||
|
||||
@@ -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 "МоёРасширение"
|
||||
```
|
||||
|
||||
@@ -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"
|
||||
```
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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 "ЗапуститьОбновление"
|
||||
```
|
||||
|
||||
@@ -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 "МоёРасширение"
|
||||
```
|
||||
|
||||
@@ -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"
|
||||
```
|
||||
|
||||
@@ -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"
|
||||
```
|
||||
|
||||
@@ -30,7 +30,7 @@ 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>"]
|
||||
```
|
||||
|
||||
## Дальнейшие шаги
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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"
|
||||
```
|
||||
|
||||
|
||||
@@ -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"
|
||||
```
|
||||
|
||||
@@ -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"
|
||||
```
|
||||
|
||||
@@ -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]
|
||||
```
|
||||
|
||||
## Дальнейшие шаги
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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"
|
||||
```
|
||||
|
||||
|
||||
@@ -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 — назначение формы
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# form-add v1.4 — 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 ---
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# form-add v1.4 — 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
|
||||
|
||||
@@ -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 — справка
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# form-compile v1.20 — Compile 1C managed form from JSON or object metadata
|
||||
# form-compile v1.21 — Compile 1C managed form from JSON or object metadata
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[string]$JsonPath,
|
||||
@@ -1912,6 +1912,7 @@ function Emit-Element {
|
||||
# input-specific
|
||||
"multiLine"=1;"passwordMode"=1;"choiceButton"=1;"clearButton"=1
|
||||
"spinButton"=1;"dropListButton"=1;"markIncomplete"=1;"skipOnInput"=1;"inputHint"=1
|
||||
"textEdit"=1
|
||||
# label/hyperlink
|
||||
"hyperlink"=1
|
||||
# group-specific
|
||||
@@ -2137,6 +2138,7 @@ function Emit-Input {
|
||||
if ($el.spinButton -eq $true) { X "$inner<SpinButton>true</SpinButton>" }
|
||||
if ($el.dropListButton -eq $true) { X "$inner<DropListButton>true</DropListButton>" }
|
||||
if ($el.markIncomplete -eq $true) { X "$inner<AutoMarkIncomplete>true</AutoMarkIncomplete>" }
|
||||
if ($el.textEdit -eq $false) { X "$inner<TextEdit>false</TextEdit>" }
|
||||
if ($el.skipOnInput -eq $true) { X "$inner<SkipOnInput>true</SkipOnInput>" }
|
||||
$hasAmw = $el.PSObject.Properties.Name -contains 'autoMaxWidth'
|
||||
if ($hasAmw) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# form-compile v1.20 — Compile 1C managed form from JSON or object metadata
|
||||
# form-compile v1.21 — Compile 1C managed form from JSON or object metadata
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
import argparse
|
||||
import copy
|
||||
@@ -1350,6 +1350,7 @@ KNOWN_KEYS = {
|
||||
"maxWidth", "maxHeight",
|
||||
"multiLine", "passwordMode", "choiceButton", "clearButton",
|
||||
"spinButton", "dropListButton", "markIncomplete", "skipOnInput", "inputHint",
|
||||
"textEdit",
|
||||
"hyperlink",
|
||||
"showTitle", "united", "collapsed",
|
||||
"children", "columns",
|
||||
@@ -1940,6 +1941,8 @@ def emit_input(lines, el, name, eid, indent):
|
||||
lines.append(f'{inner}<DropListButton>true</DropListButton>')
|
||||
if el.get('markIncomplete') is True:
|
||||
lines.append(f'{inner}<AutoMarkIncomplete>true</AutoMarkIncomplete>')
|
||||
if el.get('textEdit') is False:
|
||||
lines.append(f'{inner}<TextEdit>false</TextEdit>')
|
||||
if el.get('skipOnInput') is True:
|
||||
lines.append(f'{inner}<SkipOnInput>true</SkipOnInput>')
|
||||
if 'autoMaxWidth' in el:
|
||||
|
||||
@@ -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 формат
|
||||
|
||||
@@ -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>"
|
||||
```
|
||||
|
||||
## Параметры
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
```
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# form-validate v1.4 — Validate 1C managed form
|
||||
# form-validate v1.6 — Validate 1C managed form
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
@@ -366,13 +366,51 @@ if (-not $stopped) {
|
||||
$dataPath = $dpNode.InnerText.Trim()
|
||||
if (-not $dataPath) { continue }
|
||||
|
||||
# Opaque platform-internal DataPath shapes — not validatable from Form.xml alone:
|
||||
# - bare numeric (e.g. "10", "1000003") — internal index
|
||||
# - "N/M:<uuid>" — metadata reference by UUID
|
||||
if ($dataPath -match '^\d+$' -or $dataPath -match '^\d+/\d+:[0-9a-fA-F-]+$') {
|
||||
continue
|
||||
}
|
||||
|
||||
$pathChecked++
|
||||
|
||||
# Extract root segment of path, strip array indices like [0]
|
||||
$cleanPath = $dataPath -replace '\[\d+\]', ''
|
||||
# Strip leading '~' (current row of DynamicList: ~Список.Поле)
|
||||
if ($cleanPath.StartsWith('~')) { $cleanPath = $cleanPath.Substring(1) }
|
||||
$segments = $cleanPath -split '\.'
|
||||
$rootAttr = $segments[0]
|
||||
|
||||
# Resolve Items.<TableName>.CurrentData.<Field>... — table element, not attribute
|
||||
if ($rootAttr -eq 'Items') {
|
||||
if ($segments.Count -lt 3 -or $segments[2] -ne 'CurrentData') {
|
||||
Report-Warn "[$tag] '$elName': DataPath='$dataPath' — unknown Items.* shape, expected Items.<Table>.CurrentData.*"
|
||||
continue
|
||||
}
|
||||
$tableName = $segments[1]
|
||||
$tableEl = $null
|
||||
foreach ($candidate in $allElements) {
|
||||
if ($candidate.Tag -eq 'Table' -and $candidate.Name -eq $tableName) {
|
||||
$tableEl = $candidate
|
||||
break
|
||||
}
|
||||
}
|
||||
if (-not $tableEl) {
|
||||
Report-Error "[$tag] '$elName': DataPath='$dataPath' — table element '$tableName' not found"
|
||||
$pathErrors++
|
||||
continue
|
||||
}
|
||||
$tableDpNode = $tableEl.Node.SelectSingleNode("f:DataPath", $nsMgr)
|
||||
if (-not $tableDpNode -or -not $tableDpNode.InnerText.Trim()) {
|
||||
# Table without DataPath — can't resolve further, accept silently
|
||||
continue
|
||||
}
|
||||
$tableDp = $tableDpNode.InnerText.Trim() -replace '\[\d+\]', ''
|
||||
if ($tableDp.StartsWith('~')) { $tableDp = $tableDp.Substring(1) }
|
||||
$rootAttr = ($tableDp -split '\.')[0]
|
||||
}
|
||||
|
||||
if (-not $attrMap.ContainsKey($rootAttr)) {
|
||||
Report-Error "[$tag] '$elName': DataPath='$dataPath' — attribute '$rootAttr' not found"
|
||||
$pathErrors++
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# form-validate v1.4 — Validate 1C managed form
|
||||
# form-validate v1.6 — Validate 1C managed form
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
@@ -376,12 +376,44 @@ def main():
|
||||
if not data_path:
|
||||
continue
|
||||
|
||||
# Opaque platform-internal DataPath shapes — not validatable from Form.xml alone:
|
||||
# - bare numeric (e.g. "10", "1000003") — internal index
|
||||
# - "N/M:<uuid>" — metadata reference by UUID
|
||||
if re.match(r'^\d+$', data_path) or re.match(r'^\d+/\d+:[0-9a-fA-F-]+$', data_path):
|
||||
continue
|
||||
|
||||
path_checked += 1
|
||||
|
||||
clean_path = re.sub(r'\[\d+\]', '', data_path)
|
||||
# Strip leading '~' (current row of DynamicList: ~\u0421\u043f\u0438\u0441\u043e\u043a.\u041f\u043e\u043b\u0435)
|
||||
if clean_path.startswith('~'):
|
||||
clean_path = clean_path[1:]
|
||||
segments = clean_path.split(".")
|
||||
root_attr = segments[0]
|
||||
|
||||
# Resolve Items.<TableName>.CurrentData.<Field>... \u2014 table element, not attribute
|
||||
if root_attr == 'Items':
|
||||
if len(segments) < 3 or segments[2] != 'CurrentData':
|
||||
report_warn(f"[{tag}] '{el_name}': DataPath='{data_path}' \u2014 unknown Items.* shape, expected Items.<Table>.CurrentData.*")
|
||||
continue
|
||||
table_name = segments[1]
|
||||
table_el = None
|
||||
for candidate in all_elements:
|
||||
if candidate["Tag"] == 'Table' and candidate["Name"] == table_name:
|
||||
table_el = candidate
|
||||
break
|
||||
if table_el is None:
|
||||
report_error(f"[{tag}] '{el_name}': DataPath='{data_path}' \u2014 table element '{table_name}' not found")
|
||||
path_errors += 1
|
||||
continue
|
||||
table_dp_node = table_el["Node"].find(f"{{{F_NS}}}DataPath")
|
||||
if table_dp_node is None or not (table_dp_node.text or "").strip():
|
||||
continue
|
||||
table_dp = re.sub(r'\[\d+\]', '', (table_dp_node.text or "").strip())
|
||||
if table_dp.startswith('~'):
|
||||
table_dp = table_dp[1:]
|
||||
root_attr = table_dp.split(".")[0]
|
||||
|
||||
if root_attr not in attr_map:
|
||||
report_error(f"[{tag}] '{el_name}': DataPath='{data_path}' \u2014 attribute '{root_attr}' not found")
|
||||
path_errors += 1
|
||||
|
||||
@@ -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>"]
|
||||
```
|
||||
|
||||
## Что делает скрипт
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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`).
|
||||
|
||||
@@ -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>'
|
||||
```
|
||||
|
||||
## Операции
|
||||
|
||||
@@ -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"
|
||||
```
|
||||
|
||||
@@ -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>"
|
||||
```
|
||||
|
||||
| Параметр | Описание |
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# meta-compile v1.11 — Compile 1C metadata object from JSON
|
||||
# meta-compile v1.12 — Compile 1C metadata object from JSON
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
@@ -502,6 +502,7 @@ function Parse-AttributeShorthand {
|
||||
fillChecking = if ($val.fillChecking) { "$($val.fillChecking)" } else { "" }
|
||||
indexing = if ($val.indexing) { "$($val.indexing)" } else { "" }
|
||||
multiLine = if ($val.multiLine -eq $true) { $true } else { $false }
|
||||
choiceHistoryOnInput = if ($val.choiceHistoryOnInput) { "$($val.choiceHistoryOnInput)" } else { "" }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -822,7 +823,8 @@ function Emit-Attribute {
|
||||
X "$indent`t`t<CreateOnInput>Auto</CreateOnInput>"
|
||||
X "$indent`t`t<ChoiceForm/>"
|
||||
X "$indent`t`t<LinkByType/>"
|
||||
X "$indent`t`t<ChoiceHistoryOnInput>Auto</ChoiceHistoryOnInput>"
|
||||
$chi = if ($parsed.choiceHistoryOnInput) { $parsed.choiceHistoryOnInput } else { "Auto" }
|
||||
X "$indent`t`t<ChoiceHistoryOnInput>$chi</ChoiceHistoryOnInput>"
|
||||
|
||||
# Use — only for catalog top-level attributes
|
||||
if ($context -eq "catalog") {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# meta-compile v1.11 — Compile 1C metadata object from JSON
|
||||
# meta-compile v1.12 — Compile 1C metadata object from JSON
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
@@ -465,6 +465,7 @@ def parse_attribute_shorthand(val):
|
||||
'fillChecking': str(val['fillChecking']) if val.get('fillChecking') else '',
|
||||
'indexing': str(val['indexing']) if val.get('indexing') else '',
|
||||
'multiLine': True if val.get('multiLine') is True else False,
|
||||
'choiceHistoryOnInput': str(val['choiceHistoryOnInput']) if val.get('choiceHistoryOnInput') else '',
|
||||
}
|
||||
|
||||
def parse_enum_value_shorthand(val):
|
||||
@@ -774,7 +775,8 @@ def emit_attribute(indent, parsed, context):
|
||||
X(f'{indent}\t\t<CreateOnInput>Auto</CreateOnInput>')
|
||||
X(f'{indent}\t\t<ChoiceForm/>')
|
||||
X(f'{indent}\t\t<LinkByType/>')
|
||||
X(f'{indent}\t\t<ChoiceHistoryOnInput>Auto</ChoiceHistoryOnInput>')
|
||||
chi = parsed.get('choiceHistoryOnInput') or 'Auto'
|
||||
X(f'{indent}\t\t<ChoiceHistoryOnInput>{chi}</ChoiceHistoryOnInput>')
|
||||
if context == 'catalog':
|
||||
X(f'{indent}\t\t<Use>ForItem</Use>')
|
||||
if context not in ('processor', 'processor-tabular'):
|
||||
|
||||
@@ -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>"
|
||||
```
|
||||
|
||||
| Параметр | Описание |
|
||||
|
||||
@@ -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 "<путь>"
|
||||
```
|
||||
|
||||
## Три режима
|
||||
|
||||
@@ -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.Товары"
|
||||
```
|
||||
|
||||
## Поддерживаемые типы
|
||||
|
||||
@@ -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/Заказ"
|
||||
```
|
||||
|
||||
@@ -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"
|
||||
```
|
||||
|
||||
## Рабочий процесс
|
||||
|
||||
@@ -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"]
|
||||
```
|
||||
|
||||
## Рабочий процесс
|
||||
|
||||
@@ -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 "<каталог>"]
|
||||
```
|
||||
|
||||
Дополнительные флаги:
|
||||
|
||||
@@ -23,7 +23,7 @@ allowed-tools:
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File .claude/skills/mxl-validate/scripts/mxl-validate.ps1 -TemplatePath "Catalogs/Номенклатура/Templates/Макет"
|
||||
powershell.exe -NoProfile -File .claude/skills/mxl-validate/scripts/mxl-validate.ps1 -TemplatePath "src/МояОбработка/Templates/ПечатнаяФорма"
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/mxl-validate.ps1" -TemplatePath "Catalogs/Номенклатура/Templates/Макет"
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/mxl-validate.ps1" -TemplatePath "src/МояОбработка/Templates/ПечатнаяФорма"
|
||||
```
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ allowed-tools:
|
||||
| `OutputDir` | Корень выгрузки конфигурации (где `Configuration.xml`, `Roles/` и т.д.) |
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File .claude/skills/role-compile/scripts/role-compile.ps1 -JsonPath "<json>" -OutputDir "<ConfigDir>"
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/role-compile.ps1" -JsonPath "<json>" -OutputDir "<ConfigDir>"
|
||||
```
|
||||
|
||||
Создаёт `{OutputDir}/Roles/Имя.xml` и `{OutputDir}/Roles/Имя/Ext/Rights.xml`. Регистрирует `<Role>` в `Configuration.xml`.
|
||||
|
||||
@@ -22,7 +22,7 @@ allowed-tools:
|
||||
## Запуск скрипта
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File .claude/skills/role-info/scripts/role-info.ps1 -RightsPath <path> -OutFile <output.txt>
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/role-info.ps1" -RightsPath <path> -OutFile <output.txt>
|
||||
```
|
||||
|
||||
### Параметры
|
||||
|
||||
@@ -23,5 +23,5 @@ allowed-tools:
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File .claude/skills/role-validate/scripts/role-validate.ps1 -RightsPath "Roles/МояРоль"
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/role-validate.ps1" -RightsPath "Roles/МояРоль"
|
||||
```
|
||||
|
||||
@@ -23,10 +23,10 @@ allowed-tools:
|
||||
|
||||
```powershell
|
||||
# Из файла
|
||||
powershell.exe -NoProfile -File .claude/skills/skd-compile/scripts/skd-compile.ps1 -DefinitionFile "<json>" -OutputPath "<Template.xml>"
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/skd-compile.ps1" -DefinitionFile "<json>" -OutputPath "<Template.xml>"
|
||||
|
||||
# Из строки (без промежуточного файла)
|
||||
powershell.exe -NoProfile -File .claude/skills/skd-compile/scripts/skd-compile.ps1 -Value '<json-string>' -OutputPath "<Template.xml>"
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/skd-compile.ps1" -Value '<json-string>' -OutputPath "<Template.xml>"
|
||||
```
|
||||
|
||||
## JSON DSL — краткий справочник
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# skd-compile v1.21 — Compile 1C DCS from JSON
|
||||
# skd-compile v1.22 — Compile 1C DCS from JSON
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[string]$DefinitionFile,
|
||||
@@ -1130,6 +1130,8 @@ function Emit-ParamValue {
|
||||
X "$indent<value xsi:type=`"xs:decimal`">$(Esc-Xml $valStr)</value>"
|
||||
} elseif ($type -match '^string') {
|
||||
X "$indent<value xsi:type=`"xs:string`">$(Esc-Xml $valStr)</value>"
|
||||
} elseif ($type -match '^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|BusinessProcessRef|TaskRef|ExchangePlanRef)\.') {
|
||||
X "$indent<value xsi:type=`"dcscor:DesignTimeValue`">$(Esc-Xml $valStr)</value>"
|
||||
} else {
|
||||
# Guess from value
|
||||
if ($valStr -match '^\d{4}-\d{2}-\d{2}T') {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# skd-compile v1.21 — Compile 1C DCS from JSON
|
||||
# skd-compile v1.22 — Compile 1C DCS from JSON
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
import argparse
|
||||
import json
|
||||
@@ -811,6 +811,8 @@ def emit_param_value(lines, type_str, val, indent):
|
||||
lines.append(f'{indent}<value xsi:type="xs:decimal">{esc_xml(val_str)}</value>')
|
||||
elif type_str and re.match(r'^string', type_str):
|
||||
lines.append(f'{indent}<value xsi:type="xs:string">{esc_xml(val_str)}</value>')
|
||||
elif type_str and re.match(r'^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|BusinessProcessRef|TaskRef|ExchangePlanRef)\.', type_str):
|
||||
lines.append(f'{indent}<value xsi:type="dcscor:DesignTimeValue">{esc_xml(val_str)}</value>')
|
||||
else:
|
||||
# Guess from value
|
||||
if re.match(r'^\d{4}-\d{2}-\d{2}T', val_str):
|
||||
|
||||
@@ -25,7 +25,7 @@ allowed-tools:
|
||||
| `NoSelection` | (опц.) Не добавлять поле в selection варианта |
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File .claude/skills/skd-edit/scripts/skd-edit.ps1 -TemplatePath "<path>" -Operation <op> -Value "<value>"
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/skd-edit.ps1" -TemplatePath "<path>" -Operation <op> -Value "<value>"
|
||||
```
|
||||
|
||||
## Пакетный режим (batch)
|
||||
@@ -54,9 +54,12 @@ Shorthand: `"Имя [Заголовок]: тип @роль #ограничени
|
||||
|
||||
### add-total — добавить итог
|
||||
|
||||
Shorthand: `"<dataPath>: <выражение>"`. Если выражение — известная аггрегатная функция без скобок (`Сумма`, `Количество`, `Минимум`, `Максимум`, `Среднее`), оно автоматически оборачивается в `Func(dataPath)`. Если функция со скобками или произвольное выражение — используется как есть.
|
||||
|
||||
```
|
||||
"Цена: Среднее"
|
||||
"Стоимость: Сумма(Кол * Цена)"
|
||||
"Цена: Среднее" # → Среднее(Цена)
|
||||
"Стоимость: Сумма(Кол * Цена)" # → как есть
|
||||
"Проверка: Проверка" # identity: выражение = Проверка
|
||||
```
|
||||
|
||||
### add-calculated-field — добавить вычисляемое поле
|
||||
@@ -80,24 +83,46 @@ Shorthand: `"Имя [Заголовок]: тип = Выражение #noFilter
|
||||
"Организация: CatalogRef.Организации"
|
||||
```
|
||||
|
||||
Shorthand: `"Имя [Заголовок]: тип = значение @флаги"`. `[Заголовок]` опциональный — добавляет `<title>`.
|
||||
Shorthand: `"Имя [Заголовок]: тип = значение [availableValue=список] [@флаги]"`. `[Заголовок]` опциональный — добавляет `<title>`.
|
||||
|
||||
`@autoDates` генерирует пару скрытых параметров `ДатаНачала`/`ДатаОкончания` для StandardPeriod-параметра — для БСП-отчётов, чтобы получить пару полей «Начало/Конец» в панели быстрых настроек.
|
||||
Флаги:
|
||||
- `@autoDates` — генерирует пару скрытых параметров `ДатаНачала`/`ДатаОкончания` для StandardPeriod-параметра.
|
||||
- `@hidden` — скрывает параметр от пользовательских настроек (для параметров-констант, используемых в запросе).
|
||||
- `@always` — параметр всегда подставляется в запрос. Часто вместе с `@hidden`, но используется и отдельно (для видимых обязательных параметров типа отчётного периода).
|
||||
|
||||
```
|
||||
"ПС: CatalogRef.Контрагенты = Справочник.Контрагенты.ПустаяСсылка @hidden"
|
||||
"Период: StandardPeriod = LastMonth @always"
|
||||
"ПСчет: ChartOfAccountsRef.Хозрасчетный = ПланСчетов.Хозрасчетный.X @hidden @always"
|
||||
"Округление: EnumRef.Округления = Окр1 availableValue=Перечисление.Округления.Окр1: руб., Перечисление.Округления.Окр1000: тыс."
|
||||
```
|
||||
|
||||
`availableValue=` задаёт начальный список допустимых значений. Формат списка: `v1[: p1], v2[: p2], ...` — элементы через `,`, представление после `:`. Если в значении или представлении встречается `,` или `:` — оборачивай в одинарные кавычки `'...'`:
|
||||
|
||||
```
|
||||
"Округление: ... = Окр1 availableValue=Окр1_00: 'руб., коп.', Окр1: руб."
|
||||
```
|
||||
|
||||
### modify-parameter — изменить существующий параметр
|
||||
|
||||
Находит параметр по имени, добавляет/обновляет свойства.
|
||||
Shorthand: `"ИмяПараметра [Заголовок] [ключ=значение]... [@флаги]"`. Находит параметр по имени, обновляет указанные свойства.
|
||||
|
||||
```
|
||||
"ПорядокОкругления use=Always"
|
||||
"ПорядокОкругления [Округление сумм] denyIncompleteValues=true"
|
||||
"ПериодОтчета [Отчетный период]" # только title
|
||||
"ПорядокОкругления availableValue=Перечисление.Округления.Окр1 presentation=руб."
|
||||
"ПорядокОкругления availableValue=Перечисление.Округления.Окр1: руб., Перечисление.Округления.Окр1000: тыс."
|
||||
"СчетПС value=ПланСчетов.Хозрасчетный.КассаПредприятия"
|
||||
"Контрагент @hidden @always"
|
||||
```
|
||||
|
||||
`[Заголовок]` опциональный — устанавливает или заменяет `<title>`. Можно вызывать без других kv-пар, чтобы только обновить title.
|
||||
|
||||
`availableValue=` добавляет один элемент списка допустимых значений (можно несколько через `;;`). Тип значения определяется автоматически (DesignTimeValue для ссылок).
|
||||
`availableValue=` **заменяет весь список** допустимых значений (старые удаляются). Формат и кавычки — те же, что в `add-parameter`.
|
||||
|
||||
`value=` заменяет значение параметра (тип значения подбирается автоматически по объявленному типу параметра).
|
||||
|
||||
Флаги `@hidden` / `@always` — те же, что и в `add-parameter`. Идемпотентны.
|
||||
|
||||
### rename-parameter — переименовать параметр
|
||||
|
||||
@@ -231,13 +256,17 @@ Value — имена ресурсов (как в полях/вычисляемы
|
||||
|
||||
### patch-query — точечная замена в тексте запроса
|
||||
|
||||
Shorthand: `"старое => новое"`. Заменяет все вхождения подстроки. Поддерживает пакетный режим и `-DataSet`.
|
||||
Shorthand: `"старое => новое [@once]"`. По умолчанию заменяет все вхождения подстроки. Поддерживает пакетный режим и `-DataSet`.
|
||||
|
||||
```
|
||||
"СубконтоДт1) В => СубконтоКт1) В"
|
||||
"ЛЕВОЕ СОЕДИНЕНИЕ => ВНУТРЕННЕЕ СОЕДИНЕНИЕ"
|
||||
"КАК ВТ_СтароеИмя => КАК ВТ_НовоеИмя @once"
|
||||
```
|
||||
|
||||
`@once` — упасть с ошибкой, если в запросе не **ровно одно** вхождение. Защищает от случайных замен в комментариях и однотипных идентификаторах.
|
||||
|
||||
Многострочные подстроки поддерживаются — переводы строк в `старое`/`новое` сравниваются буквально (включая отступы).
|
||||
|
||||
### set-outputParameter — установить параметр вывода
|
||||
|
||||
```
|
||||
@@ -249,16 +278,27 @@ Shorthand: `"старое => новое"`. Заменяет все вхожде
|
||||
|
||||
### set-structure — установить структуру варианта
|
||||
|
||||
Shorthand: `"Поле1 > Поле2 > details"`. `details`/`детали` — детальные записи. Заменяет всю структуру. Не поддерживает пакетный режим.
|
||||
Shorthand: `"Поле1 > Поле2 > details"`. `>` — вложенный уровень группировки, `,` — несколько полей в одном уровне, `details` — детальные записи. **Заменяет всю структуру полностью** (включая Selection/order/filter/conditionalAppearance каждой группы). Для точечной модификации полей группировки с сохранением настроек — используй `modify-structure`. Не поддерживает пакетный режим.
|
||||
|
||||
```
|
||||
"Организация > Номенклатура > details"
|
||||
"Валюта, НаименованиеБанка, ИНН"
|
||||
"details"
|
||||
"СчетМеждународногоУчета @name=ДанныеОтчета"
|
||||
```
|
||||
|
||||
`@name=Имя` — присваивает имя группировке (`<dcsset:name>`). Используется для привязки шаблонов через `groupName`.
|
||||
|
||||
### modify-structure — изменить поля группировки существующей группы
|
||||
|
||||
Тот же shorthand что и `set-structure`. Находит группу по `@name=`, заменяет только `<groupItems>` (поля группировки). Selection/order/filter/conditionalAppearance/outputParameters группы сохраняются. Без `@name=` — ошибка.
|
||||
|
||||
```
|
||||
"Валюта @name=ДанныеОтчета"
|
||||
"Валюта, НаименованиеБанка @name=ДанныеОтчета"
|
||||
"details @name=ДанныеОтчета"
|
||||
```
|
||||
|
||||
### modify-field — изменить существующее поле
|
||||
|
||||
Тот же shorthand что и `add-field`. Находит по dataPath, объединяет свойства (непустые переопределяют), сохраняет позицию.
|
||||
@@ -267,6 +307,23 @@ Shorthand: `"Поле1 > Поле2 > details"`. `details`/`детали` — д
|
||||
"Цена [Цена USD]: decimal(10,4) @dimension"
|
||||
```
|
||||
|
||||
### set-field-role — установить роль поля
|
||||
|
||||
Shorthand: `"<dataPath> [@флаги] [kv=значение]"`. **Полностью заменяет** роль поля. Если в значении только dataPath без флагов/kv — удаляет роль.
|
||||
|
||||
```
|
||||
"Сумма" # снять роль полностью
|
||||
"СуммаОстаток @balance" # простая балансовая роль
|
||||
"СуммаНач @balance balanceGroupName=Сумма balanceType=OpeningBalance" # с уточнением
|
||||
"Контрагент @dimension parentDimension=Группа"
|
||||
"Период @period" # роль периода
|
||||
```
|
||||
|
||||
Флаги: `@balance`, `@dimension`, `@account`, `@period`, `@required`, `@autoOrder`, `@ignoreNullValues`.
|
||||
KV: `balanceGroupName`, `balanceType` (OpeningBalance/ClosingBalance), `parentDimension`, `accountTypeExpression`, `orderType` (Asc/Desc), `expression`, `periodNumber`, `periodType`.
|
||||
|
||||
Поддерживает пакетный режим (`;;`).
|
||||
|
||||
### modify-filter — изменить существующий фильтр
|
||||
|
||||
Тот же shorthand что и `add-filter`. Находит по полю, обновляет оператор/значение/флаги. См. правило для `<use>` ниже.
|
||||
@@ -294,6 +351,7 @@ Shorthand: `"Поле1 > Поле2 > details"`. `details`/`детали` — д
|
||||
| `clear-selection` | `*` | Очищает все элементы selection |
|
||||
| `clear-order` | `*` | Очищает все элементы order |
|
||||
| `clear-filter` | `*` | Очищает все элементы filter |
|
||||
| `clear-conditionalAppearance` | `*` | Очищает все правила условного оформления |
|
||||
|
||||
## Верификация
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# skd-edit v1.11 — Atomic 1C DCS editor
|
||||
# skd-edit v1.18 — Atomic 1C DCS editor
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
@@ -11,9 +11,9 @@ param(
|
||||
"add-dataParameter","add-order","add-selection","add-dataSetLink",
|
||||
"add-dataSet","add-variant","add-conditionalAppearance","add-drilldown",
|
||||
"set-query","patch-query","set-outputParameter","set-structure",
|
||||
"modify-field","modify-filter","modify-dataParameter","modify-parameter",
|
||||
"modify-field","modify-filter","modify-dataParameter","modify-parameter","modify-structure","set-field-role",
|
||||
"rename-parameter","reorder-parameters",
|
||||
"clear-selection","clear-order","clear-filter",
|
||||
"clear-selection","clear-order","clear-filter","clear-conditionalAppearance",
|
||||
"remove-field","remove-total","remove-calculated-field","remove-parameter","remove-filter")]
|
||||
[string]$Operation,
|
||||
|
||||
@@ -238,14 +238,25 @@ function Read-FieldProperties($fieldEl) {
|
||||
function Parse-TotalShorthand {
|
||||
param([string]$s)
|
||||
|
||||
# "DataPath: Func" or "DataPath: Func(expr)" or "DataPath: ИмяРесурса" (identity)
|
||||
$parts = $s -split ':', 2
|
||||
$dataPath = $parts[0].Trim()
|
||||
$funcPart = $parts[1].Trim()
|
||||
|
||||
# Known DCS aggregate functions (ru + en)
|
||||
$aggFuncs = @('Сумма','Количество','Минимум','Максимум','Среднее',
|
||||
'Sum','Count','Min','Max','Avg',
|
||||
'Minimum','Maximum','Average')
|
||||
|
||||
if ($funcPart -match '^\w+\(') {
|
||||
# Already has expression form: Func(expr)
|
||||
return @{ dataPath = $dataPath; expression = $funcPart }
|
||||
} else {
|
||||
} elseif ($funcPart -in $aggFuncs) {
|
||||
# Short: Func → Func(DataPath)
|
||||
return @{ dataPath = $dataPath; expression = "$funcPart($dataPath)" }
|
||||
} else {
|
||||
# Identity or custom expression — use as-is
|
||||
return @{ dataPath = $dataPath; expression = $funcPart }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -297,13 +308,29 @@ function Parse-CalcShorthand {
|
||||
function Parse-ParamShorthand {
|
||||
param([string]$s)
|
||||
|
||||
$result = @{ name = ""; type = ""; value = $null; autoDates = $false; title = $null }
|
||||
$result = @{ name = ""; type = ""; value = $null; autoDates = $false; title = $null; hidden = $false; always = $false; availableValues = @() }
|
||||
|
||||
# Extract availableValue=... (must be before main parse — captures to end of string)
|
||||
if ($s -match '\s*availableValue=(.+)$') {
|
||||
$result.availableValues = Parse-AvailableValueList $Matches[1].Trim()
|
||||
$s = ($s -replace '\s*availableValue=.+$', '').Trim()
|
||||
}
|
||||
|
||||
if ($s -match '@autoDates') {
|
||||
$result.autoDates = $true
|
||||
$s = $s -replace '\s*@autoDates', ''
|
||||
}
|
||||
|
||||
if ($s -match '@hidden\b') {
|
||||
$result.hidden = $true
|
||||
$s = $s -replace '\s*@hidden\b', ''
|
||||
}
|
||||
|
||||
if ($s -match '@always\b') {
|
||||
$result.always = $true
|
||||
$s = $s -replace '\s*@always\b', ''
|
||||
}
|
||||
|
||||
# Extract optional [Title] (mirrors Parse-FieldShorthand)
|
||||
if ($s -match '\[([^\]]*)\]') {
|
||||
$result.title = $Matches[1].Trim()
|
||||
@@ -581,15 +608,17 @@ function Parse-StructureShorthand {
|
||||
$seg = $segments[$i].Trim()
|
||||
$group = @{ type = "group" }
|
||||
|
||||
if ($seg -match '@name=(.+)') {
|
||||
$group["name"] = $Matches[1].Trim()
|
||||
$seg = ($seg -replace '\s*@name=.+', '').Trim()
|
||||
if ($seg -match '@name=(?:"([^"]+)"|''([^'']+)''|(\S+))') {
|
||||
$rawName = if ($Matches[1]) { $Matches[1] } elseif ($Matches[2]) { $Matches[2] } else { $Matches[3] }
|
||||
$group["name"] = $rawName.Trim()
|
||||
$seg = ($seg -replace '\s*@name=(?:"[^"]+"|''[^'']+''|\S+)', '').Trim()
|
||||
}
|
||||
|
||||
if ($seg -match '^(?i)(details|детали)$') {
|
||||
$group["groupBy"] = @()
|
||||
} else {
|
||||
$group["groupBy"] = @($seg)
|
||||
$fields = @($seg -split '\s*,\s*' | ForEach-Object { $_.Trim() } | Where-Object { $_ })
|
||||
$group["groupBy"] = $fields
|
||||
}
|
||||
|
||||
if ($null -ne $innermost) {
|
||||
@@ -614,6 +643,75 @@ function Parse-OutputParamShorthand {
|
||||
return @{ key = $s.Trim(); value = "" }
|
||||
}
|
||||
|
||||
function Parse-AvailableValueList {
|
||||
# Returns array of @{ value=...; presentation=... } from comma-separated list.
|
||||
# Items can use 'single' or "double" quotes (stripped). Quoted spans preserve commas/colons.
|
||||
param([string]$s)
|
||||
|
||||
$result = @()
|
||||
if (-not $s) { return ,$result }
|
||||
|
||||
# Tokenize by ',' respecting quoted spans
|
||||
$items = @()
|
||||
$buf = New-Object System.Text.StringBuilder
|
||||
$inQuote = $null
|
||||
for ($i = 0; $i -lt $s.Length; $i++) {
|
||||
$ch = $s[$i]
|
||||
if ($inQuote) {
|
||||
[void]$buf.Append($ch)
|
||||
if ($ch -eq $inQuote) { $inQuote = $null }
|
||||
} elseif ($ch -eq "'" -or $ch -eq '"') {
|
||||
$inQuote = $ch
|
||||
[void]$buf.Append($ch)
|
||||
} elseif ($ch -eq ',') {
|
||||
$items += $buf.ToString()
|
||||
[void]$buf.Clear()
|
||||
} else {
|
||||
[void]$buf.Append($ch)
|
||||
}
|
||||
}
|
||||
if ($buf.Length -gt 0) { $items += $buf.ToString() }
|
||||
|
||||
# For each item: split into value[:presentation], strip quotes
|
||||
$stripQuotes = {
|
||||
param($t)
|
||||
$t = $t.Trim()
|
||||
if ($t.Length -ge 2 -and (($t[0] -eq "'" -and $t[-1] -eq "'") -or ($t[0] -eq '"' -and $t[-1] -eq '"'))) {
|
||||
return $t.Substring(1, $t.Length - 2)
|
||||
}
|
||||
return $t
|
||||
}
|
||||
|
||||
foreach ($raw in $items) {
|
||||
$item = $raw.Trim()
|
||||
if (-not $item) { continue }
|
||||
|
||||
# Find first ':' outside quotes
|
||||
$colonIdx = -1
|
||||
$q = $null
|
||||
for ($j = 0; $j -lt $item.Length; $j++) {
|
||||
$c = $item[$j]
|
||||
if ($q) {
|
||||
if ($c -eq $q) { $q = $null }
|
||||
} elseif ($c -eq "'" -or $c -eq '"') {
|
||||
$q = $c
|
||||
} elseif ($c -eq ':') {
|
||||
$colonIdx = $j; break
|
||||
}
|
||||
}
|
||||
|
||||
if ($colonIdx -ge 0) {
|
||||
$valPart = $item.Substring(0, $colonIdx)
|
||||
$presPart = $item.Substring($colonIdx + 1)
|
||||
$result += @{ value = (& $stripQuotes $valPart); presentation = (& $stripQuotes $presPart) }
|
||||
} else {
|
||||
$result += @{ value = (& $stripQuotes $item); presentation = "" }
|
||||
}
|
||||
}
|
||||
|
||||
return ,$result
|
||||
}
|
||||
|
||||
# --- 4. Build-* functions (XML fragment generators) ---
|
||||
|
||||
function Build-ValueTypeXml {
|
||||
@@ -804,6 +902,68 @@ function Build-CalcFieldFragment {
|
||||
return $lines -join "`r`n"
|
||||
}
|
||||
|
||||
function Build-ParamValueXml {
|
||||
# Returns array of XML lines for a <value xsi:type=...>...</value> element (or StandardPeriod block).
|
||||
# Selects xsi:type by declared type, then falls back to value pattern.
|
||||
param([string]$type, [string]$value, [string]$indent, [string]$tagName = "value", [string]$tagNs = "")
|
||||
|
||||
$i = $indent
|
||||
$valStr = "$value"
|
||||
$open = if ($tagNs) { "$tagNs`:$tagName" } else { $tagName }
|
||||
$lines = @()
|
||||
|
||||
if ($type -eq "StandardPeriod") {
|
||||
$lines += "$i<$open xsi:type=`"v8:StandardPeriod`">"
|
||||
$lines += "$i`t<v8:variant xsi:type=`"v8:StandardPeriodVariant`">$(Esc-Xml $valStr)</v8:variant>"
|
||||
$lines += "$i`t<v8:startDate>0001-01-01T00:00:00</v8:startDate>"
|
||||
$lines += "$i`t<v8:endDate>0001-01-01T00:00:00</v8:endDate>"
|
||||
$lines += "$i</$open>"
|
||||
return $lines
|
||||
}
|
||||
|
||||
$xsi = $null
|
||||
if ($type -match '^date') { $xsi = "xs:dateTime" }
|
||||
elseif ($type -eq "boolean") { $xsi = "xs:boolean" }
|
||||
elseif ($type -match '^decimal') { $xsi = "xs:decimal" }
|
||||
elseif ($type -match '^string') { $xsi = "xs:string" }
|
||||
elseif ($type -match '^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|BusinessProcessRef|TaskRef|ExchangePlanRef)\.') {
|
||||
$xsi = "dcscor:DesignTimeValue"
|
||||
}
|
||||
else {
|
||||
# Type unknown or empty — guess from value
|
||||
if ($valStr -match '^\d{4}-\d{2}-\d{2}T') { $xsi = "xs:dateTime" }
|
||||
elseif ($valStr -eq "true" -or $valStr -eq "false") { $xsi = "xs:boolean" }
|
||||
elseif ($valStr -match '^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена)\.' -or
|
||||
$valStr -match '^(Catalog|Document|Enum|ChartOfAccounts|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.') {
|
||||
$xsi = "dcscor:DesignTimeValue"
|
||||
}
|
||||
else { $xsi = "xs:string" }
|
||||
}
|
||||
|
||||
$lines += "$i<$open xsi:type=`"$xsi`">$(Esc-Xml $valStr)</$open>"
|
||||
return $lines
|
||||
}
|
||||
|
||||
function Build-AvailableValueFragment {
|
||||
# Returns XML lines (array) for a single <availableValue> block.
|
||||
param($item, [string]$declaredType, [string]$indent)
|
||||
|
||||
$lines = @()
|
||||
$lines += "$indent<availableValue>"
|
||||
$valueLines = Build-ParamValueXml -type $declaredType -value $item.value -indent "$indent`t"
|
||||
foreach ($vl in $valueLines) { $lines += $vl }
|
||||
if ($item.presentation) {
|
||||
$lines += "$indent`t<presentation xsi:type=`"v8:LocalStringType`">"
|
||||
$lines += "$indent`t`t<v8:item>"
|
||||
$lines += "$indent`t`t`t<v8:lang>ru</v8:lang>"
|
||||
$lines += "$indent`t`t`t<v8:content>$(Esc-Xml $item.presentation)</v8:content>"
|
||||
$lines += "$indent`t`t</v8:item>"
|
||||
$lines += "$indent`t</presentation>"
|
||||
}
|
||||
$lines += "$indent</availableValue>"
|
||||
return $lines
|
||||
}
|
||||
|
||||
function Build-ParamFragment {
|
||||
param($parsed, [string]$indent)
|
||||
|
||||
@@ -825,24 +985,26 @@ function Build-ParamFragment {
|
||||
}
|
||||
|
||||
if ($null -ne $parsed.value) {
|
||||
$valStr = "$($parsed.value)"
|
||||
if ($parsed.type -eq "StandardPeriod") {
|
||||
$lines += "$i`t<value xsi:type=`"v8:StandardPeriod`">"
|
||||
$lines += "$i`t`t<v8:variant xsi:type=`"v8:StandardPeriodVariant`">$(Esc-Xml $valStr)</v8:variant>"
|
||||
$lines += "$i`t`t<v8:startDate>0001-01-01T00:00:00</v8:startDate>"
|
||||
$lines += "$i`t`t<v8:endDate>0001-01-01T00:00:00</v8:endDate>"
|
||||
$lines += "$i`t</value>"
|
||||
} elseif ($parsed.type -match '^date') {
|
||||
$lines += "$i`t<value xsi:type=`"xs:dateTime`">$(Esc-Xml $valStr)</value>"
|
||||
} elseif ($parsed.type -eq "boolean") {
|
||||
$lines += "$i`t<value xsi:type=`"xs:boolean`">$(Esc-Xml $valStr)</value>"
|
||||
} elseif ($parsed.type -match '^decimal') {
|
||||
$lines += "$i`t<value xsi:type=`"xs:decimal`">$(Esc-Xml $valStr)</value>"
|
||||
} else {
|
||||
$lines += "$i`t<value xsi:type=`"xs:string`">$(Esc-Xml $valStr)</value>"
|
||||
$valueLines = Build-ParamValueXml -type $parsed.type -value $parsed.value -indent "$i`t"
|
||||
foreach ($vl in $valueLines) { $lines += $vl }
|
||||
}
|
||||
|
||||
if ($parsed.hidden) {
|
||||
$lines += "$i`t<useRestriction>true</useRestriction>"
|
||||
$lines += "$i`t<availableAsField>false</availableAsField>"
|
||||
}
|
||||
|
||||
if ($parsed.availableValues -and $parsed.availableValues.Count -gt 0) {
|
||||
foreach ($av in $parsed.availableValues) {
|
||||
$avLines = Build-AvailableValueFragment -item $av -declaredType $parsed.type -indent "$i`t"
|
||||
foreach ($l in $avLines) { $lines += $l }
|
||||
}
|
||||
}
|
||||
|
||||
if ($parsed.always) {
|
||||
$lines += "$i`t<use>Always</use>"
|
||||
}
|
||||
|
||||
$lines += "$i</parameter>"
|
||||
$fragments += ($lines -join "`r`n")
|
||||
|
||||
@@ -1565,10 +1727,10 @@ $corNs = "http://v8.1c.ru/8.1/data-composition-system/core"
|
||||
|
||||
# --- 7. Batch value splitting ---
|
||||
|
||||
if ($Operation -eq "set-query" -or $Operation -eq "set-structure" -or $Operation -eq "add-dataSet") {
|
||||
if ($Operation -eq "set-query" -or $Operation -eq "set-structure" -or $Operation -eq "modify-structure" -or $Operation -eq "add-dataSet") {
|
||||
$values = @($Value)
|
||||
} elseif ($Operation -eq "patch-query") {
|
||||
$values = @($Value -split ';;' | Where-Object { $_.Trim() })
|
||||
$values = @($Value -split ';;' | ForEach-Object { $_.Trim() } | Where-Object { $_ })
|
||||
} elseif ($Operation -eq "add-drilldown") {
|
||||
if ($Value.Contains(';;')) {
|
||||
$values = @($Value -split ';;' | ForEach-Object { $_.Trim() } | Where-Object { $_ })
|
||||
@@ -1766,6 +1928,12 @@ switch ($Operation) {
|
||||
$paramName = $parts[0].Trim()
|
||||
$rest = if ($parts.Count -gt 1) { $parts[1].Trim() } else { "" }
|
||||
|
||||
# Extract @hidden / @always flags
|
||||
$flagHidden = $false
|
||||
$flagAlways = $false
|
||||
if ($rest -match '@hidden\b') { $flagHidden = $true; $rest = ($rest -replace '\s*@hidden\b', '').Trim() }
|
||||
if ($rest -match '@always\b') { $flagAlways = $true; $rest = ($rest -replace '\s*@always\b', '').Trim() }
|
||||
|
||||
# Find parameter element
|
||||
$paramEl = Find-ElementByChildValue $xmlDoc.DocumentElement "parameter" "name" $paramName $schNs
|
||||
if (-not $paramEl) {
|
||||
@@ -1810,15 +1978,63 @@ switch ($Operation) {
|
||||
$avPart = $rest.Substring($avIdx)
|
||||
}
|
||||
|
||||
# Process simple key=value pairs (use, denyIncompleteValues, etc.)
|
||||
# Process simple key=value pairs (use, denyIncompleteValues, value, etc.)
|
||||
if ($simpleRest) {
|
||||
$kvPairs = [regex]::Matches($simpleRest, '(\w+)=(\S+)')
|
||||
foreach ($kv in $kvPairs) {
|
||||
$key = $kv.Groups[1].Value
|
||||
$value = $kv.Groups[2].Value
|
||||
|
||||
$existing = $paramEl.SelectSingleNode($key)
|
||||
if ($existing) {
|
||||
# Namespace-aware lookup (children live in $schNs)
|
||||
$existing = $null
|
||||
foreach ($ch in $paramEl.ChildNodes) {
|
||||
if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq $key -and $ch.NamespaceURI -eq $schNs) {
|
||||
$existing = $ch; break
|
||||
}
|
||||
}
|
||||
|
||||
if ($key -eq "value") {
|
||||
# Special-case: rebuild <value> with correct xsi:type from <valueType>
|
||||
$declaredType = ""
|
||||
$vtEl = $null
|
||||
foreach ($ch in $paramEl.ChildNodes) {
|
||||
if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'valueType' -and $ch.NamespaceURI -eq $schNs) { $vtEl = $ch; break }
|
||||
}
|
||||
if ($vtEl) {
|
||||
foreach ($tnode in $vtEl.ChildNodes) {
|
||||
if ($tnode.NodeType -eq 'Element' -and $tnode.LocalName -eq 'Type') {
|
||||
$declaredType = $tnode.InnerText.Trim() -replace '^d\d+p\d+:', ''
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
$valueLines = Build-ParamValueXml -type $declaredType -value $value -indent $childIndent
|
||||
$fragXml = $valueLines -join "`r`n"
|
||||
|
||||
$wasExisting = ($null -ne $existing)
|
||||
if ($existing) {
|
||||
# Capture position by next-element sibling, then remove existing
|
||||
$refNode = $existing.NextSibling
|
||||
while ($refNode -and ($refNode.NodeType -eq 'Whitespace' -or $refNode.NodeType -eq 'SignificantWhitespace')) {
|
||||
$refNode = $refNode.NextSibling
|
||||
}
|
||||
Remove-NodeWithWhitespace $existing
|
||||
} else {
|
||||
# Insert before useRestriction/availableValue/denyIncompleteValues/use
|
||||
$refNode = $null
|
||||
foreach ($child in $paramEl.ChildNodes) {
|
||||
if ($child.NodeType -eq 'Element' -and $child.LocalName -in @('useRestriction','availableValue','denyIncompleteValues','use')) {
|
||||
$refNode = $child; break
|
||||
}
|
||||
}
|
||||
}
|
||||
$nodes = Import-Fragment $xmlDoc $fragXml
|
||||
foreach ($node in $nodes) {
|
||||
Insert-BeforeElement $paramEl $node $refNode $childIndent
|
||||
}
|
||||
$verb = if ($wasExisting) { "updated" } else { "added" }
|
||||
Write-Host "[OK] Parameter `"$paramName`": value $verb to $value"
|
||||
} elseif ($existing) {
|
||||
$existing.InnerText = $value
|
||||
Write-Host "[OK] Parameter `"$paramName`": $key updated to $value"
|
||||
} else {
|
||||
@@ -1841,46 +2057,102 @@ switch ($Operation) {
|
||||
}
|
||||
}
|
||||
|
||||
# Process availableValue
|
||||
# Process availableValue — replace whole list with new items
|
||||
if ($avPart) {
|
||||
$avRest = $avPart -replace '^availableValue=', ''
|
||||
# Parse: "Перечисление...X presentation=текст с пробелами"
|
||||
$avParts = $avRest -split '\s+presentation=', 2
|
||||
$avValue = $avParts[0].Trim()
|
||||
$avPresentation = if ($avParts.Count -gt 1) { $avParts[1].Trim() } else { "" }
|
||||
$avRest = ($avPart -replace '^availableValue=', '').Trim()
|
||||
$avItems = Parse-AvailableValueList $avRest
|
||||
|
||||
# Detect value type
|
||||
$avType = "xs:string"
|
||||
if ($avValue -match '^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета)\.') {
|
||||
$avType = "dcscor:DesignTimeValue"
|
||||
# Detect value type: prefer declared <valueType> of the parameter, else guess from value
|
||||
$declaredType = ""
|
||||
$vtEl = $null
|
||||
foreach ($ch in $paramEl.ChildNodes) {
|
||||
if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'valueType' -and $ch.NamespaceURI -eq $schNs) { $vtEl = $ch; break }
|
||||
}
|
||||
if ($vtEl) {
|
||||
foreach ($tnode in $vtEl.ChildNodes) {
|
||||
if ($tnode.NodeType -eq 'Element' -and $tnode.LocalName -eq 'Type') {
|
||||
$declaredType = $tnode.InnerText.Trim() -replace '^d\d+p\d+:', ''
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$avLines = @()
|
||||
$avLines += "$childIndent<availableValue>"
|
||||
$avLines += "$childIndent`t<value xsi:type=`"$avType`">$(Esc-Xml $avValue)</value>"
|
||||
if ($avPresentation) {
|
||||
$avLines += "$childIndent`t<presentation xsi:type=`"v8:LocalStringType`">"
|
||||
$avLines += "$childIndent`t`t<v8:item>"
|
||||
$avLines += "$childIndent`t`t`t<v8:lang>ru</v8:lang>"
|
||||
$avLines += "$childIndent`t`t`t<v8:content>$(Esc-Xml $avPresentation)</v8:content>"
|
||||
$avLines += "$childIndent`t`t</v8:item>"
|
||||
$avLines += "$childIndent`t</presentation>"
|
||||
# Remove all existing <availableValue> elements
|
||||
$toRemove = @()
|
||||
foreach ($ch in $paramEl.ChildNodes) {
|
||||
if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'availableValue' -and $ch.NamespaceURI -eq $schNs) {
|
||||
$toRemove += $ch
|
||||
}
|
||||
}
|
||||
$avLines += "$childIndent</availableValue>"
|
||||
$fragXml = $avLines -join "`r`n"
|
||||
foreach ($el in $toRemove) { Remove-NodeWithWhitespace $el }
|
||||
|
||||
# Insert before first of (denyIncompleteValues, use) in document order
|
||||
# Insert each new <availableValue> before (denyIncompleteValues, use)
|
||||
$refNode = $null
|
||||
foreach ($child in $paramEl.ChildNodes) {
|
||||
if ($child.NodeType -eq 'Element' -and ($child.LocalName -eq 'denyIncompleteValues' -or $child.LocalName -eq 'use')) {
|
||||
$refNode = $child; break
|
||||
}
|
||||
}
|
||||
$nodes = Import-Fragment $xmlDoc $fragXml
|
||||
foreach ($node in $nodes) {
|
||||
Insert-BeforeElement $paramEl $node $refNode $childIndent
|
||||
foreach ($av in $avItems) {
|
||||
$avLines = Build-AvailableValueFragment -item $av -declaredType $declaredType -indent $childIndent
|
||||
$fragXml = $avLines -join "`r`n"
|
||||
$nodes = Import-Fragment $xmlDoc $fragXml
|
||||
foreach ($node in $nodes) {
|
||||
Insert-BeforeElement $paramEl $node $refNode $childIndent
|
||||
}
|
||||
}
|
||||
Write-Host "[OK] Parameter `"$paramName`": availableValue added"
|
||||
Write-Host "[OK] Parameter `"$paramName`": availableValue set to $($avItems.Count) item(s)"
|
||||
}
|
||||
|
||||
# Process @hidden / @always flags (idempotent)
|
||||
if ($flagHidden) {
|
||||
# useRestriction → true (insert after <value>, before <expression>/<availableAsField>/...)
|
||||
$urEl = $null
|
||||
foreach ($ch in $paramEl.ChildNodes) {
|
||||
if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'useRestriction' -and $ch.NamespaceURI -eq $schNs) { $urEl = $ch; break }
|
||||
}
|
||||
if ($urEl) {
|
||||
if ($urEl.InnerText.Trim() -ne 'true') { $urEl.InnerText = 'true' }
|
||||
} else {
|
||||
$refNode = $null
|
||||
foreach ($child in $paramEl.ChildNodes) {
|
||||
if ($child.NodeType -eq 'Element' -and $child.LocalName -in @('expression','availableAsField','availableValue','denyIncompleteValues','use')) { $refNode = $child; break }
|
||||
}
|
||||
$nodes = Import-Fragment $xmlDoc "$childIndent<useRestriction>true</useRestriction>"
|
||||
foreach ($node in $nodes) { Insert-BeforeElement $paramEl $node $refNode $childIndent }
|
||||
}
|
||||
|
||||
# availableAsField → false (insert after <expression>, before <availableValue>/<denyIncompleteValues>/<use>)
|
||||
$afEl = $null
|
||||
foreach ($ch in $paramEl.ChildNodes) {
|
||||
if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'availableAsField' -and $ch.NamespaceURI -eq $schNs) { $afEl = $ch; break }
|
||||
}
|
||||
if ($afEl) {
|
||||
if ($afEl.InnerText.Trim() -ne 'false') { $afEl.InnerText = 'false' }
|
||||
} else {
|
||||
$refNode = $null
|
||||
foreach ($child in $paramEl.ChildNodes) {
|
||||
if ($child.NodeType -eq 'Element' -and $child.LocalName -in @('availableValue','denyIncompleteValues','use')) { $refNode = $child; break }
|
||||
}
|
||||
$nodes = Import-Fragment $xmlDoc "$childIndent<availableAsField>false</availableAsField>"
|
||||
foreach ($node in $nodes) { Insert-BeforeElement $paramEl $node $refNode $childIndent }
|
||||
}
|
||||
|
||||
Write-Host "[OK] Parameter `"$paramName`": @hidden applied"
|
||||
}
|
||||
|
||||
if ($flagAlways) {
|
||||
$useEl = $null
|
||||
foreach ($ch in $paramEl.ChildNodes) {
|
||||
if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'use' -and $ch.NamespaceURI -eq $schNs) { $useEl = $ch; break }
|
||||
}
|
||||
if ($useEl) {
|
||||
if ($useEl.InnerText.Trim() -ne 'Always') { $useEl.InnerText = 'Always' }
|
||||
} else {
|
||||
$nodes = Import-Fragment $xmlDoc "$childIndent<use>Always</use>"
|
||||
foreach ($node in $nodes) { Insert-BeforeElement $paramEl $node $null $childIndent }
|
||||
}
|
||||
Write-Host "[OK] Parameter `"$paramName`": @always applied"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2211,6 +2483,12 @@ switch ($Operation) {
|
||||
}
|
||||
|
||||
foreach ($val in $values) {
|
||||
$once = $false
|
||||
if ($val -match '@once\b') {
|
||||
$once = $true
|
||||
$val = ($val -replace '\s*@once\b', '').Trim()
|
||||
}
|
||||
|
||||
$sepIdx = $val.IndexOf(" => ")
|
||||
if ($sepIdx -lt 0) {
|
||||
Write-Error "patch-query value must contain ' => ' separator: old => new"
|
||||
@@ -2219,12 +2497,20 @@ switch ($Operation) {
|
||||
$oldStr = $val.Substring(0, $sepIdx)
|
||||
$newStr = $val.Substring($sepIdx + 4)
|
||||
$queryText = $queryEl.InnerText
|
||||
if (-not $queryText.Contains($oldStr)) {
|
||||
|
||||
$count = ([regex]::Matches($queryText, [regex]::Escape($oldStr))).Count
|
||||
if ($count -eq 0) {
|
||||
Write-Error "Substring not found in query of dataset '$dsName': $oldStr"
|
||||
exit 1
|
||||
}
|
||||
if ($once -and $count -ne 1) {
|
||||
Write-Error "@once: expected 1 occurrence of '$oldStr' in dataset '$dsName', found $count"
|
||||
exit 1
|
||||
}
|
||||
|
||||
$queryEl.InnerText = $queryText.Replace($oldStr, $newStr)
|
||||
Write-Host "[OK] Query patched in dataset `"$dsName`": replaced '$oldStr'"
|
||||
$suffix = if ($once) { " (1 occurrence)" } else { " ($count occurrence(s))" }
|
||||
Write-Host "[OK] Query patched in dataset `"$dsName`": replaced '$oldStr'$suffix"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2289,6 +2575,102 @@ switch ($Operation) {
|
||||
Write-Host "[OK] Structure set in variant `"$varName`": $Value"
|
||||
}
|
||||
|
||||
"modify-structure" {
|
||||
$settings = Resolve-VariantSettings
|
||||
$varName = Get-VariantName
|
||||
|
||||
$structItems = Parse-StructureShorthand $Value
|
||||
|
||||
# Flatten parsed tree into (name, groupBy) targets
|
||||
$targets = @()
|
||||
$stack = New-Object System.Collections.Stack
|
||||
foreach ($it in $structItems) { $stack.Push($it) }
|
||||
while ($stack.Count -gt 0) {
|
||||
$it = $stack.Pop()
|
||||
if ($it["name"]) {
|
||||
$targets += @{ name = $it["name"]; groupBy = $it["groupBy"] }
|
||||
}
|
||||
if ($it["children"]) {
|
||||
foreach ($ch in $it["children"]) { $stack.Push($ch) }
|
||||
}
|
||||
}
|
||||
|
||||
if ($targets.Count -eq 0) {
|
||||
Write-Error "modify-structure requires @name= for at least one group: $Value"
|
||||
exit 1
|
||||
}
|
||||
|
||||
$nsMgr = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable)
|
||||
$nsMgr.AddNamespace("dcsset", $setNs)
|
||||
$nsMgr.AddNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance")
|
||||
|
||||
foreach ($t in $targets) {
|
||||
$groupEl = $settings.SelectSingleNode(".//dcsset:item[@xsi:type='dcsset:StructureItemGroup'][dcsset:name='$($t.name)']", $nsMgr)
|
||||
if (-not $groupEl) {
|
||||
Write-Host "[WARN] Group with @name=`"$($t.name)`" not found — skipped"
|
||||
continue
|
||||
}
|
||||
|
||||
$giEl = $null
|
||||
foreach ($ch in $groupEl.ChildNodes) {
|
||||
if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'groupItems' -and $ch.NamespaceURI -eq $setNs) {
|
||||
$giEl = $ch; break
|
||||
}
|
||||
}
|
||||
$groupIndent = Get-ChildIndent $groupEl
|
||||
if (-not $giEl) {
|
||||
# Create <groupItems> after <name>, before <order>/<selection>/...
|
||||
$nameEl = $null
|
||||
$refAfterName = $null
|
||||
foreach ($ch in $groupEl.ChildNodes) {
|
||||
if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'name' -and $ch.NamespaceURI -eq $setNs) {
|
||||
$nameEl = $ch
|
||||
} elseif ($ch.NodeType -eq 'Element' -and $nameEl -and -not $refAfterName) {
|
||||
$refAfterName = $ch; break
|
||||
}
|
||||
}
|
||||
$giFrag = "$groupIndent<dcsset:groupItems></dcsset:groupItems>"
|
||||
$nodes = Import-Fragment $xmlDoc $giFrag
|
||||
foreach ($node in $nodes) {
|
||||
Insert-BeforeElement $groupEl $node $refAfterName $groupIndent
|
||||
}
|
||||
# Re-find
|
||||
foreach ($ch in $groupEl.ChildNodes) {
|
||||
if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'groupItems' -and $ch.NamespaceURI -eq $setNs) {
|
||||
$giEl = $ch; break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$toRemove = @()
|
||||
foreach ($ch in $giEl.ChildNodes) {
|
||||
if ($ch.NodeType -eq 'Element') { $toRemove += $ch }
|
||||
}
|
||||
foreach ($el in $toRemove) { Remove-NodeWithWhitespace $el }
|
||||
|
||||
$itemIndent = "$groupIndent`t"
|
||||
|
||||
foreach ($field in $t.groupBy) {
|
||||
$lines = @()
|
||||
$lines += "$itemIndent<dcsset:item xsi:type=`"dcsset:GroupItemField`">"
|
||||
$lines += "$itemIndent`t<dcsset:field>$(Esc-Xml $field)</dcsset:field>"
|
||||
$lines += "$itemIndent`t<dcsset:groupType>Items</dcsset:groupType>"
|
||||
$lines += "$itemIndent`t<dcsset:periodAdditionType>None</dcsset:periodAdditionType>"
|
||||
$lines += "$itemIndent`t<dcsset:periodAdditionBegin xsi:type=`"xs:dateTime`">0001-01-01T00:00:00</dcsset:periodAdditionBegin>"
|
||||
$lines += "$itemIndent`t<dcsset:periodAdditionEnd xsi:type=`"xs:dateTime`">0001-01-01T00:00:00</dcsset:periodAdditionEnd>"
|
||||
$lines += "$itemIndent</dcsset:item>"
|
||||
$fragXml = $lines -join "`r`n"
|
||||
$nodes = Import-Fragment $xmlDoc $fragXml
|
||||
foreach ($node in $nodes) {
|
||||
Insert-BeforeElement $giEl $node $null $itemIndent
|
||||
}
|
||||
}
|
||||
|
||||
$desc = if ($t.groupBy.Count -eq 0) { "details" } else { $t.groupBy -join ', ' }
|
||||
Write-Host "[OK] Group `"$($t.name)`" groupItems updated: $desc"
|
||||
}
|
||||
}
|
||||
|
||||
"add-dataSetLink" {
|
||||
foreach ($val in $values) {
|
||||
$parsed = Parse-DataSetLinkShorthand $val
|
||||
@@ -2476,6 +2858,18 @@ switch ($Operation) {
|
||||
}
|
||||
}
|
||||
|
||||
"clear-conditionalAppearance" {
|
||||
$settings = Resolve-VariantSettings
|
||||
$varName = Get-VariantName
|
||||
$caEl = Find-FirstElement $settings @("conditionalAppearance") $setNs
|
||||
if ($caEl) {
|
||||
Clear-ContainerChildren $caEl
|
||||
Write-Host "[OK] ConditionalAppearance cleared in variant `"$varName`""
|
||||
} else {
|
||||
Write-Host "[INFO] No conditionalAppearance section in variant `"$varName`""
|
||||
}
|
||||
}
|
||||
|
||||
"modify-filter" {
|
||||
$settings = Resolve-VariantSettings
|
||||
$varName = Get-VariantName
|
||||
@@ -2675,6 +3069,87 @@ switch ($Operation) {
|
||||
}
|
||||
}
|
||||
|
||||
"set-field-role" {
|
||||
$dsNode = Resolve-DataSet
|
||||
$dsName = Get-DataSetName $dsNode
|
||||
|
||||
foreach ($val in $values) {
|
||||
# Parse shorthand: "dataPath [@flag ...] [kv=value ...]"
|
||||
$s = $val.Trim()
|
||||
|
||||
# Extract @flags
|
||||
$flags = @()
|
||||
$flagMatches = [regex]::Matches($s, '@(\w+)')
|
||||
foreach ($m in $flagMatches) { $flags += $m.Groups[1].Value }
|
||||
$s = [regex]::Replace($s, '\s*@\w+', '').Trim()
|
||||
|
||||
# Extract kv=value (value is non-whitespace)
|
||||
$kv = [ordered]@{}
|
||||
$kvMatches = [regex]::Matches($s, '(\w+)=(\S+)')
|
||||
foreach ($m in $kvMatches) { $kv[$m.Groups[1].Value] = $m.Groups[2].Value }
|
||||
$s = [regex]::Replace($s, '\s*\w+=\S+', '').Trim()
|
||||
|
||||
$dataPath = $s
|
||||
if (-not $dataPath) {
|
||||
Write-Host "[WARN] set-field-role: empty dataPath in `"$val`""
|
||||
continue
|
||||
}
|
||||
|
||||
$fieldEl = Find-ElementByChildValue $dsNode "field" "dataPath" $dataPath $schNs
|
||||
if (-not $fieldEl) {
|
||||
Write-Host "[WARN] Field `"$dataPath`" not found in dataset `"$dsName`""
|
||||
continue
|
||||
}
|
||||
|
||||
$fieldIndent = Get-ChildIndent $fieldEl
|
||||
|
||||
# Remove existing <role>
|
||||
$oldRole = $null
|
||||
foreach ($ch in $fieldEl.ChildNodes) {
|
||||
if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'role' -and $ch.NamespaceURI -eq $schNs) { $oldRole = $ch; break }
|
||||
}
|
||||
if ($oldRole) { Remove-NodeWithWhitespace $oldRole }
|
||||
|
||||
# Empty spec — remove only
|
||||
if ($flags.Count -eq 0 -and $kv.Count -eq 0) {
|
||||
Write-Host "[OK] Field `"$dataPath`" role cleared"
|
||||
continue
|
||||
}
|
||||
|
||||
# Build new <role>
|
||||
$lines = @()
|
||||
$lines += "$fieldIndent<role>"
|
||||
foreach ($flag in $flags) {
|
||||
if ($flag -eq 'period') {
|
||||
$lines += "$fieldIndent`t<dcscom:periodNumber>1</dcscom:periodNumber>"
|
||||
$lines += "$fieldIndent`t<dcscom:periodType>Main</dcscom:periodType>"
|
||||
} else {
|
||||
$lines += "$fieldIndent`t<dcscom:$flag>true</dcscom:$flag>"
|
||||
}
|
||||
}
|
||||
foreach ($k in $kv.Keys) {
|
||||
$lines += "$fieldIndent`t<dcscom:$k>$(Esc-Xml $kv[$k])</dcscom:$k>"
|
||||
}
|
||||
$lines += "$fieldIndent</role>"
|
||||
$fragXml = $lines -join "`r`n"
|
||||
|
||||
# Insert before <valueType>, else before <inputParameters>, else at end
|
||||
$refNode = $null
|
||||
foreach ($ch in $fieldEl.ChildNodes) {
|
||||
if ($ch.NodeType -eq 'Element' -and $ch.LocalName -in @('valueType','inputParameters') -and $ch.NamespaceURI -eq $schNs) { $refNode = $ch; break }
|
||||
}
|
||||
$nodes = Import-Fragment $xmlDoc $fragXml
|
||||
foreach ($node in $nodes) {
|
||||
Insert-BeforeElement $fieldEl $node $refNode $fieldIndent
|
||||
}
|
||||
|
||||
$desc = @()
|
||||
if ($flags.Count -gt 0) { $desc += ($flags | ForEach-Object { "@$_" }) -join ' ' }
|
||||
if ($kv.Count -gt 0) { $desc += ($kv.Keys | ForEach-Object { "$_=$($kv[$_])" }) -join ' ' }
|
||||
Write-Host "[OK] Field `"$dataPath`" role set: $($desc -join ' ')"
|
||||
}
|
||||
}
|
||||
|
||||
"remove-field" {
|
||||
$dsNode = Resolve-DataSet
|
||||
$dsName = Get-DataSetName $dsNode
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# skd-edit v1.11 — Atomic 1C DCS editor (Python port)
|
||||
# skd-edit v1.18 — Atomic 1C DCS editor (Python port)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
import argparse
|
||||
import os
|
||||
@@ -18,9 +18,9 @@ VALID_OPS = [
|
||||
"add-dataParameter", "add-order", "add-selection", "add-dataSetLink",
|
||||
"add-dataSet", "add-variant", "add-conditionalAppearance", "add-drilldown",
|
||||
"set-query", "patch-query", "set-outputParameter", "set-structure",
|
||||
"modify-field", "modify-filter", "modify-dataParameter", "modify-parameter",
|
||||
"modify-field", "modify-filter", "modify-dataParameter", "modify-parameter", "modify-structure", "set-field-role",
|
||||
"rename-parameter", "reorder-parameters",
|
||||
"clear-selection", "clear-order", "clear-filter",
|
||||
"clear-selection", "clear-order", "clear-filter", "clear-conditionalAppearance",
|
||||
"remove-field", "remove-total", "remove-calculated-field", "remove-parameter", "remove-filter",
|
||||
]
|
||||
|
||||
@@ -250,13 +250,21 @@ def read_field_properties(field_el):
|
||||
|
||||
|
||||
def parse_total_shorthand(s):
|
||||
# "DataPath: Func" or "DataPath: Func(expr)" or "DataPath: ИмяРесурса" (identity)
|
||||
parts = s.split(":", 1)
|
||||
data_path = parts[0].strip()
|
||||
func_part = parts[1].strip()
|
||||
|
||||
agg_funcs = {'Сумма', 'Количество', 'Минимум', 'Максимум', 'Среднее',
|
||||
'Sum', 'Count', 'Min', 'Max', 'Avg',
|
||||
'Minimum', 'Maximum', 'Average'}
|
||||
|
||||
if re.match(r'^\w+\(', func_part):
|
||||
return {"dataPath": data_path, "expression": func_part}
|
||||
else:
|
||||
elif func_part in agg_funcs:
|
||||
return {"dataPath": data_path, "expression": f"{func_part}({data_path})"}
|
||||
else:
|
||||
return {"dataPath": data_path, "expression": func_part}
|
||||
|
||||
|
||||
def parse_calc_shorthand(s):
|
||||
@@ -299,12 +307,26 @@ def parse_calc_shorthand(s):
|
||||
|
||||
|
||||
def parse_param_shorthand(s):
|
||||
result = {"name": "", "type": "", "value": None, "autoDates": False, "title": None}
|
||||
result = {"name": "", "type": "", "value": None, "autoDates": False, "title": None, "hidden": False, "always": False, "availableValues": []}
|
||||
|
||||
# Extract availableValue=... (must be before main parse — captures to end of string)
|
||||
m_av = re.search(r'\s*availableValue=(.+)$', s)
|
||||
if m_av:
|
||||
result["availableValues"] = parse_available_value_list(m_av.group(1).strip())
|
||||
s = re.sub(r'\s*availableValue=.+$', '', s).strip()
|
||||
|
||||
if re.search(r'@autoDates', s):
|
||||
result["autoDates"] = True
|
||||
s = re.sub(r'\s*@autoDates', '', s)
|
||||
|
||||
if re.search(r'@hidden\b', s):
|
||||
result["hidden"] = True
|
||||
s = re.sub(r'\s*@hidden\b', '', s)
|
||||
|
||||
if re.search(r'@always\b', s):
|
||||
result["always"] = True
|
||||
s = re.sub(r'\s*@always\b', '', s)
|
||||
|
||||
# Extract optional [Title] (mirrors parse_field_shorthand)
|
||||
m = re.search(r'\[([^\]]*)\]', s)
|
||||
if m:
|
||||
@@ -545,15 +567,17 @@ def parse_structure_shorthand(s):
|
||||
seg = segments[i].strip()
|
||||
group = {"type": "group"}
|
||||
|
||||
name_m = re.search(r'\s*@name=(.+)', seg)
|
||||
name_m = re.search(r'@name=(?:"([^"]+)"|\'([^\']+)\'|(\S+))', seg)
|
||||
if name_m:
|
||||
group["name"] = name_m.group(1).strip()
|
||||
seg = re.sub(r'\s*@name=.+', '', seg).strip()
|
||||
raw_name = name_m.group(1) or name_m.group(2) or name_m.group(3)
|
||||
group["name"] = raw_name.strip()
|
||||
seg = re.sub(r'\s*@name=(?:"[^"]+"|\'[^\']+\'|\S+)', '', seg).strip()
|
||||
|
||||
if re.match(r'^(details|\u0434\u0435\u0442\u0430\u043b\u0438)$', seg, re.IGNORECASE):
|
||||
group["groupBy"] = []
|
||||
else:
|
||||
group["groupBy"] = [seg]
|
||||
fields = [f.strip() for f in re.split(r'\s*,\s*', seg) if f.strip()]
|
||||
group["groupBy"] = fields
|
||||
|
||||
if innermost is not None:
|
||||
group["children"] = [innermost]
|
||||
@@ -571,6 +595,80 @@ def parse_output_param_shorthand(s):
|
||||
return {"key": s.strip(), "value": ""}
|
||||
|
||||
|
||||
def parse_available_value_list(s):
|
||||
"""Returns list of {value, presentation} from comma-separated list.
|
||||
Items can use single/double quotes (stripped). Quoted spans preserve commas/colons."""
|
||||
if not s:
|
||||
return []
|
||||
|
||||
# Tokenize by ',' respecting quoted spans
|
||||
items = []
|
||||
buf = []
|
||||
in_quote = None
|
||||
for ch in s:
|
||||
if in_quote:
|
||||
buf.append(ch)
|
||||
if ch == in_quote:
|
||||
in_quote = None
|
||||
elif ch in ("'", '"'):
|
||||
in_quote = ch
|
||||
buf.append(ch)
|
||||
elif ch == ',':
|
||||
items.append("".join(buf))
|
||||
buf = []
|
||||
else:
|
||||
buf.append(ch)
|
||||
if buf:
|
||||
items.append("".join(buf))
|
||||
|
||||
def strip_quotes(t):
|
||||
t = t.strip()
|
||||
if len(t) >= 2 and ((t[0] == "'" and t[-1] == "'") or (t[0] == '"' and t[-1] == '"')):
|
||||
return t[1:-1]
|
||||
return t
|
||||
|
||||
result = []
|
||||
for raw in items:
|
||||
item = raw.strip()
|
||||
if not item:
|
||||
continue
|
||||
# Find first ':' outside quotes
|
||||
colon_idx = -1
|
||||
q = None
|
||||
for j, c in enumerate(item):
|
||||
if q:
|
||||
if c == q:
|
||||
q = None
|
||||
elif c in ("'", '"'):
|
||||
q = c
|
||||
elif c == ':':
|
||||
colon_idx = j
|
||||
break
|
||||
if colon_idx >= 0:
|
||||
val_part = item[:colon_idx]
|
||||
pres_part = item[colon_idx + 1:]
|
||||
result.append({"value": strip_quotes(val_part), "presentation": strip_quotes(pres_part)})
|
||||
else:
|
||||
result.append({"value": strip_quotes(item), "presentation": ""})
|
||||
return result
|
||||
|
||||
|
||||
def build_available_value_fragment(item, declared_type, indent):
|
||||
"""Return XML lines for a single <availableValue> block."""
|
||||
lines = [f"{indent}<availableValue>"]
|
||||
for vl in build_param_value_xml(declared_type, item["value"], f"{indent}\t"):
|
||||
lines.append(vl)
|
||||
if item.get("presentation"):
|
||||
lines.append(f'{indent}\t<presentation xsi:type="v8:LocalStringType">')
|
||||
lines.append(f"{indent}\t\t<v8:item>")
|
||||
lines.append(f"{indent}\t\t\t<v8:lang>ru</v8:lang>")
|
||||
lines.append(f"{indent}\t\t\t<v8:content>{esc_xml(item['presentation'])}</v8:content>")
|
||||
lines.append(f"{indent}\t\t</v8:item>")
|
||||
lines.append(f"{indent}\t</presentation>")
|
||||
lines.append(f"{indent}</availableValue>")
|
||||
return lines
|
||||
|
||||
|
||||
# ── 4. Build-* functions (XML fragment generators) ──────────
|
||||
|
||||
def build_value_type_xml(type_str, indent):
|
||||
@@ -724,6 +822,47 @@ def build_calc_field_fragment(parsed, indent):
|
||||
return "\r\n".join(lines)
|
||||
|
||||
|
||||
def build_param_value_xml(type_str, value, indent, tag_name="value", tag_ns=""):
|
||||
"""Return list of XML lines for <value xsi:type=...>...</value>."""
|
||||
val_str = "" if value is None else str(value)
|
||||
open_tag = f"{tag_ns}:{tag_name}" if tag_ns else tag_name
|
||||
lines = []
|
||||
|
||||
if type_str == "StandardPeriod":
|
||||
lines.append(f'{indent}<{open_tag} xsi:type="v8:StandardPeriod">')
|
||||
lines.append(f'{indent}\t<v8:variant xsi:type="v8:StandardPeriodVariant">{esc_xml(val_str)}</v8:variant>')
|
||||
lines.append(f"{indent}\t<v8:startDate>0001-01-01T00:00:00</v8:startDate>")
|
||||
lines.append(f"{indent}\t<v8:endDate>0001-01-01T00:00:00</v8:endDate>")
|
||||
lines.append(f"{indent}</{open_tag}>")
|
||||
return lines
|
||||
|
||||
t = type_str or ""
|
||||
xsi = None
|
||||
if t.startswith("date"):
|
||||
xsi = "xs:dateTime"
|
||||
elif t == "boolean":
|
||||
xsi = "xs:boolean"
|
||||
elif t.startswith("decimal"):
|
||||
xsi = "xs:decimal"
|
||||
elif t.startswith("string"):
|
||||
xsi = "xs:string"
|
||||
elif re.match(r'^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|BusinessProcessRef|TaskRef|ExchangePlanRef)\.', t):
|
||||
xsi = "dcscor:DesignTimeValue"
|
||||
else:
|
||||
if re.match(r'^\d{4}-\d{2}-\d{2}T', val_str):
|
||||
xsi = "xs:dateTime"
|
||||
elif val_str in ("true", "false"):
|
||||
xsi = "xs:boolean"
|
||||
elif re.match(r'^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена)\.', val_str) or \
|
||||
re.match(r'^(Catalog|Document|Enum|ChartOfAccounts|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.', val_str):
|
||||
xsi = "dcscor:DesignTimeValue"
|
||||
else:
|
||||
xsi = "xs:string"
|
||||
|
||||
lines.append(f'{indent}<{open_tag} xsi:type="{xsi}">{esc_xml(val_str)}</{open_tag}>')
|
||||
return lines
|
||||
|
||||
|
||||
def build_param_fragment(parsed, indent):
|
||||
i = indent
|
||||
fragments = []
|
||||
@@ -739,21 +878,19 @@ def build_param_fragment(parsed, indent):
|
||||
lines.append(f"{i}\t</valueType>")
|
||||
|
||||
if parsed["value"] is not None:
|
||||
val_str = str(parsed["value"])
|
||||
if parsed.get("type") == "StandardPeriod":
|
||||
lines.append(f'{i}\t<value xsi:type="v8:StandardPeriod">')
|
||||
lines.append(f'{i}\t\t<v8:variant xsi:type="v8:StandardPeriodVariant">{esc_xml(val_str)}</v8:variant>')
|
||||
lines.append(f"{i}\t\t<v8:startDate>0001-01-01T00:00:00</v8:startDate>")
|
||||
lines.append(f"{i}\t\t<v8:endDate>0001-01-01T00:00:00</v8:endDate>")
|
||||
lines.append(f"{i}\t</value>")
|
||||
elif parsed.get("type", "").startswith("date"):
|
||||
lines.append(f'{i}\t<value xsi:type="xs:dateTime">{esc_xml(val_str)}</value>')
|
||||
elif parsed.get("type") == "boolean":
|
||||
lines.append(f'{i}\t<value xsi:type="xs:boolean">{esc_xml(val_str)}</value>')
|
||||
elif parsed.get("type", "").startswith("decimal"):
|
||||
lines.append(f'{i}\t<value xsi:type="xs:decimal">{esc_xml(val_str)}</value>')
|
||||
else:
|
||||
lines.append(f'{i}\t<value xsi:type="xs:string">{esc_xml(val_str)}</value>')
|
||||
for vl in build_param_value_xml(parsed.get("type", ""), parsed["value"], f"{i}\t"):
|
||||
lines.append(vl)
|
||||
|
||||
if parsed.get("hidden"):
|
||||
lines.append(f"{i}\t<useRestriction>true</useRestriction>")
|
||||
lines.append(f"{i}\t<availableAsField>false</availableAsField>")
|
||||
|
||||
for av in parsed.get("availableValues", []) or []:
|
||||
for l in build_available_value_fragment(av, parsed.get("type", ""), f"{i}\t"):
|
||||
lines.append(l)
|
||||
|
||||
if parsed.get("always"):
|
||||
lines.append(f"{i}\t<use>Always</use>")
|
||||
|
||||
lines.append(f"{i}</parameter>")
|
||||
fragments.append("\r\n".join(lines))
|
||||
@@ -1361,10 +1498,10 @@ xml_doc = tree.getroot()
|
||||
|
||||
# ── 7. Batch value splitting ────────────────────────────────
|
||||
|
||||
if operation in ("set-query", "set-structure", "add-dataSet"):
|
||||
if operation in ("set-query", "set-structure", "modify-structure", "add-dataSet"):
|
||||
values = [value_arg]
|
||||
elif operation == "patch-query":
|
||||
values = [v for v in value_arg.split(";;") if v.strip()]
|
||||
values = [v.strip() for v in value_arg.split(";;") if v.strip()]
|
||||
elif operation == "add-drilldown":
|
||||
if ";;" in value_arg:
|
||||
values = [v.strip() for v in value_arg.split(";;") if v.strip()]
|
||||
@@ -1537,6 +1674,15 @@ elif operation == "modify-parameter":
|
||||
param_name = parts[0].strip()
|
||||
rest = parts[1].strip() if len(parts) > 1 else ""
|
||||
|
||||
flag_hidden = False
|
||||
flag_always = False
|
||||
if re.search(r'@hidden\b', rest):
|
||||
flag_hidden = True
|
||||
rest = re.sub(r'\s*@hidden\b', '', rest).strip()
|
||||
if re.search(r'@always\b', rest):
|
||||
flag_always = True
|
||||
rest = re.sub(r'\s*@always\b', '', rest).strip()
|
||||
|
||||
param_el = find_element_by_child_value(xml_doc, "parameter", "name", param_name, SCH_NS)
|
||||
if param_el is None:
|
||||
print(f'[WARN] Parameter "{param_name}" not found -- skipped')
|
||||
@@ -1568,8 +1714,33 @@ elif operation == "modify-parameter":
|
||||
if simple_rest:
|
||||
for m in re.finditer(r'(\w+)=(\S+)', simple_rest):
|
||||
key, value = m.group(1), m.group(2)
|
||||
existing = next((ch for ch in param_el if isinstance(ch.tag, str) and local_name(ch) == key), None)
|
||||
if existing is not None:
|
||||
existing = next((ch for ch in param_el if isinstance(ch.tag, str) and local_name(ch) == key and etree.QName(ch.tag).namespace == SCH_NS), None)
|
||||
|
||||
if key == "value":
|
||||
# Rebuild <value> with correct xsi:type from <valueType>
|
||||
declared_type = ""
|
||||
vt_el = next((ch for ch in param_el if isinstance(ch.tag, str) and local_name(ch) == "valueType" and etree.QName(ch.tag).namespace == SCH_NS), None)
|
||||
if vt_el is not None:
|
||||
for tnode in vt_el:
|
||||
if isinstance(tnode.tag, str) and local_name(tnode) == "Type":
|
||||
declared_type = re.sub(r'^d\d+p\d+:', '', (tnode.text or "").strip())
|
||||
break
|
||||
value_lines = build_param_value_xml(declared_type, value, child_indent)
|
||||
frag_xml = "\r\n".join(value_lines)
|
||||
was_existing = existing is not None
|
||||
if existing is not None:
|
||||
# Find next-element sibling as ref before removing
|
||||
idx = list(param_el).index(existing)
|
||||
ref_node = param_el[idx + 1] if idx + 1 < len(param_el) else None
|
||||
remove_node_with_whitespace(existing)
|
||||
else:
|
||||
ref_node = next((ch for ch in param_el if isinstance(ch.tag, str) and local_name(ch) in ("useRestriction", "availableValue", "denyIncompleteValues", "use")), None)
|
||||
nodes = import_fragment(xml_doc, frag_xml)
|
||||
for node in nodes:
|
||||
insert_before_element(param_el, node, ref_node, child_indent)
|
||||
verb = "updated" if was_existing else "added"
|
||||
print(f'[OK] Parameter "{param_name}": value {verb} to {value}')
|
||||
elif existing is not None:
|
||||
existing.text = value
|
||||
print(f'[OK] Parameter "{param_name}": {key} updated to {value}')
|
||||
else:
|
||||
@@ -1585,38 +1756,71 @@ elif operation == "modify-parameter":
|
||||
|
||||
# Process availableValue
|
||||
if av_part:
|
||||
av_rest = av_part[len("availableValue="):]
|
||||
# Parse: "Перечисление...X presentation=текст с пробелами"
|
||||
av_parts = re.split(r'\s+presentation=', av_rest, 1)
|
||||
av_value = av_parts[0].strip()
|
||||
av_presentation = av_parts[1].strip() if len(av_parts) > 1 else ""
|
||||
av_rest = av_part[len("availableValue="):].strip()
|
||||
av_items = parse_available_value_list(av_rest)
|
||||
|
||||
av_type = "xs:string"
|
||||
if re.match(r'^(Перечисление|Справочник|ПланСчетов|Документ|ПланВидовХарактеристик|ПланВидовРасчета)\.', av_value):
|
||||
av_type = "dcscor:DesignTimeValue"
|
||||
# Prefer declared <valueType> of the parameter; fall back to value pattern
|
||||
declared_type = ""
|
||||
vt_el = None
|
||||
for ch in param_el:
|
||||
if isinstance(ch.tag, str) and local_name(ch) == "valueType" and etree.QName(ch.tag).namespace == SCH_NS:
|
||||
vt_el = ch
|
||||
break
|
||||
if vt_el is not None:
|
||||
for tnode in vt_el:
|
||||
if isinstance(tnode.tag, str) and local_name(tnode) == "Type":
|
||||
declared_type = re.sub(r'^d\d+p\d+:', '', (tnode.text or "").strip())
|
||||
break
|
||||
|
||||
av_lines = [f"{child_indent}<availableValue>"]
|
||||
av_lines.append(f'{child_indent}\t<value xsi:type="{av_type}">{esc_xml(av_value)}</value>')
|
||||
if av_presentation:
|
||||
av_lines.append(f'{child_indent}\t<presentation xsi:type="v8:LocalStringType">')
|
||||
av_lines.append(f"{child_indent}\t\t<v8:item>")
|
||||
av_lines.append(f"{child_indent}\t\t\t<v8:lang>ru</v8:lang>")
|
||||
av_lines.append(f"{child_indent}\t\t\t<v8:content>{esc_xml(av_presentation)}</v8:content>")
|
||||
av_lines.append(f"{child_indent}\t\t</v8:item>")
|
||||
av_lines.append(f"{child_indent}\t</presentation>")
|
||||
av_lines.append(f"{child_indent}</availableValue>")
|
||||
frag_xml = "\r\n".join(av_lines)
|
||||
# Remove all existing <availableValue>
|
||||
to_remove = [ch for ch in param_el if isinstance(ch.tag, str) and local_name(ch) == "availableValue" and etree.QName(ch.tag).namespace == SCH_NS]
|
||||
for el in to_remove:
|
||||
remove_node_with_whitespace(el)
|
||||
|
||||
# Insert before first of (denyIncompleteValues, use) in document order
|
||||
# Insert each new <availableValue> before (denyIncompleteValues, use)
|
||||
ref_node = None
|
||||
for child in param_el:
|
||||
if isinstance(child.tag, str) and local_name(child) in ("denyIncompleteValues", "use"):
|
||||
ref_node = child
|
||||
break
|
||||
nodes = import_fragment(xml_doc, frag_xml)
|
||||
for node in nodes:
|
||||
insert_before_element(param_el, node, ref_node, child_indent)
|
||||
print(f'[OK] Parameter "{param_name}": availableValue added')
|
||||
for av in av_items:
|
||||
av_lines = build_available_value_fragment(av, declared_type, child_indent)
|
||||
frag_xml = "\r\n".join(av_lines)
|
||||
nodes = import_fragment(xml_doc, frag_xml)
|
||||
for node in nodes:
|
||||
insert_before_element(param_el, node, ref_node, child_indent)
|
||||
print(f'[OK] Parameter "{param_name}": availableValue set to {len(av_items)} item(s)')
|
||||
|
||||
if flag_hidden:
|
||||
ur_el = next((ch for ch in param_el if isinstance(ch.tag, str) and local_name(ch) == "useRestriction" and etree.QName(ch.tag).namespace == SCH_NS), None)
|
||||
if ur_el is not None:
|
||||
if (ur_el.text or "").strip() != "true":
|
||||
ur_el.text = "true"
|
||||
else:
|
||||
ref_node = next((ch for ch in param_el if isinstance(ch.tag, str) and local_name(ch) in ("expression", "availableAsField", "availableValue", "denyIncompleteValues", "use")), None)
|
||||
for node in import_fragment(xml_doc, f"{child_indent}<useRestriction>true</useRestriction>"):
|
||||
insert_before_element(param_el, node, ref_node, child_indent)
|
||||
|
||||
af_el = next((ch for ch in param_el if isinstance(ch.tag, str) and local_name(ch) == "availableAsField" and etree.QName(ch.tag).namespace == SCH_NS), None)
|
||||
if af_el is not None:
|
||||
if (af_el.text or "").strip() != "false":
|
||||
af_el.text = "false"
|
||||
else:
|
||||
ref_node = next((ch for ch in param_el if isinstance(ch.tag, str) and local_name(ch) in ("availableValue", "denyIncompleteValues", "use")), None)
|
||||
for node in import_fragment(xml_doc, f"{child_indent}<availableAsField>false</availableAsField>"):
|
||||
insert_before_element(param_el, node, ref_node, child_indent)
|
||||
|
||||
print(f'[OK] Parameter "{param_name}": @hidden applied')
|
||||
|
||||
if flag_always:
|
||||
use_el = next((ch for ch in param_el if isinstance(ch.tag, str) and local_name(ch) == "use" and etree.QName(ch.tag).namespace == SCH_NS), None)
|
||||
if use_el is not None:
|
||||
if (use_el.text or "").strip() != "Always":
|
||||
use_el.text = "Always"
|
||||
else:
|
||||
for node in import_fragment(xml_doc, f"{child_indent}<use>Always</use>"):
|
||||
insert_before_element(param_el, node, None, child_indent)
|
||||
print(f'[OK] Parameter "{param_name}": @always applied')
|
||||
|
||||
elif operation == "rename-parameter":
|
||||
root = xml_doc
|
||||
@@ -1868,6 +2072,11 @@ elif operation == "patch-query":
|
||||
print(f"No <query> element found in dataset '{ds_name}'", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
for val in values:
|
||||
once = False
|
||||
if re.search(r'@once\b', val):
|
||||
once = True
|
||||
val = re.sub(r'\s*@once\b', '', val).strip()
|
||||
|
||||
sep_idx = val.find(" => ")
|
||||
if sep_idx < 0:
|
||||
print("patch-query value must contain ' => ' separator: old => new", file=sys.stderr)
|
||||
@@ -1875,11 +2084,18 @@ elif operation == "patch-query":
|
||||
old_str = val[:sep_idx]
|
||||
new_str = val[sep_idx + 4:]
|
||||
query_text = query_el.text or ""
|
||||
if old_str not in query_text:
|
||||
|
||||
count = query_text.count(old_str)
|
||||
if count == 0:
|
||||
print(f"Substring not found in query of dataset '{ds_name}': {old_str}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
if once and count != 1:
|
||||
print(f"@once: expected 1 occurrence of '{old_str}' in dataset '{ds_name}', found {count}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
query_el.text = query_text.replace(old_str, new_str)
|
||||
print(f'[OK] Query patched in dataset "{ds_name}": replaced \'{old_str}\'')
|
||||
suffix = " (1 occurrence)" if once else f" ({count} occurrence(s))"
|
||||
print(f'[OK] Query patched in dataset "{ds_name}": replaced \'{old_str}\'{suffix}')
|
||||
|
||||
elif operation == "set-outputParameter":
|
||||
settings = resolve_variant_settings()
|
||||
@@ -1922,6 +2138,85 @@ elif operation == "set-structure":
|
||||
|
||||
print(f'[OK] Structure set in variant "{var_name}": {value_arg}')
|
||||
|
||||
elif operation == "modify-structure":
|
||||
settings = resolve_variant_settings()
|
||||
var_name = get_variant_name()
|
||||
|
||||
struct_items = parse_structure_shorthand(value_arg)
|
||||
|
||||
# Flatten parsed tree into (name, groupBy) targets
|
||||
targets = []
|
||||
stack = list(struct_items)
|
||||
while stack:
|
||||
it = stack.pop()
|
||||
if it.get("name"):
|
||||
targets.append({"name": it["name"], "groupBy": it.get("groupBy", [])})
|
||||
for ch in it.get("children", []) or []:
|
||||
stack.append(ch)
|
||||
|
||||
if not targets:
|
||||
print(f"modify-structure requires @name= for at least one group: {value_arg}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
ns = {"dcsset": SET_NS, "xsi": XSI_NS}
|
||||
|
||||
for t in targets:
|
||||
xpath = f".//dcsset:item[@xsi:type='dcsset:StructureItemGroup'][dcsset:name='{t['name']}']"
|
||||
group_el = settings.find(xpath, ns)
|
||||
if group_el is None:
|
||||
print(f'[WARN] Group with @name="{t["name"]}" not found — skipped')
|
||||
continue
|
||||
|
||||
gi_el = None
|
||||
for ch in group_el:
|
||||
if isinstance(ch.tag, str) and local_name(ch) == "groupItems" and etree.QName(ch.tag).namespace == SET_NS:
|
||||
gi_el = ch
|
||||
break
|
||||
|
||||
group_indent = get_child_indent(group_el)
|
||||
if gi_el is None:
|
||||
# Insert <groupItems> after <name>, before first non-name sibling
|
||||
ref_after_name = None
|
||||
saw_name = False
|
||||
for ch in group_el:
|
||||
if not isinstance(ch.tag, str):
|
||||
continue
|
||||
if local_name(ch) == "name" and etree.QName(ch.tag).namespace == SET_NS:
|
||||
saw_name = True
|
||||
elif saw_name:
|
||||
ref_after_name = ch
|
||||
break
|
||||
gi_frag = f"{group_indent}<dcsset:groupItems></dcsset:groupItems>"
|
||||
for node in import_fragment(xml_doc, gi_frag):
|
||||
insert_before_element(group_el, node, ref_after_name, group_indent)
|
||||
for ch in group_el:
|
||||
if isinstance(ch.tag, str) and local_name(ch) == "groupItems" and etree.QName(ch.tag).namespace == SET_NS:
|
||||
gi_el = ch
|
||||
break
|
||||
|
||||
to_remove = [ch for ch in gi_el if isinstance(ch.tag, str)]
|
||||
for el in to_remove:
|
||||
remove_node_with_whitespace(el)
|
||||
|
||||
item_indent = group_indent + "\t"
|
||||
|
||||
for field in t["groupBy"]:
|
||||
lines = [
|
||||
f'{item_indent}<dcsset:item xsi:type="dcsset:GroupItemField">',
|
||||
f'{item_indent}\t<dcsset:field>{esc_xml(field)}</dcsset:field>',
|
||||
f'{item_indent}\t<dcsset:groupType>Items</dcsset:groupType>',
|
||||
f'{item_indent}\t<dcsset:periodAdditionType>None</dcsset:periodAdditionType>',
|
||||
f'{item_indent}\t<dcsset:periodAdditionBegin xsi:type="xs:dateTime">0001-01-01T00:00:00</dcsset:periodAdditionBegin>',
|
||||
f'{item_indent}\t<dcsset:periodAdditionEnd xsi:type="xs:dateTime">0001-01-01T00:00:00</dcsset:periodAdditionEnd>',
|
||||
f'{item_indent}</dcsset:item>',
|
||||
]
|
||||
frag_xml = "\r\n".join(lines)
|
||||
for node in import_fragment(xml_doc, frag_xml):
|
||||
insert_before_element(gi_el, node, None, item_indent)
|
||||
|
||||
desc = "details" if not t["groupBy"] else ", ".join(t["groupBy"])
|
||||
print(f'[OK] Group "{t["name"]}" groupItems updated: {desc}')
|
||||
|
||||
elif operation == "add-dataSetLink":
|
||||
for val in values:
|
||||
parsed = parse_data_set_link_shorthand(val)
|
||||
@@ -2085,6 +2380,16 @@ elif operation == "clear-filter":
|
||||
else:
|
||||
print(f'[INFO] No filter section in variant "{var_name}"')
|
||||
|
||||
elif operation == "clear-conditionalAppearance":
|
||||
settings = resolve_variant_settings()
|
||||
var_name = get_variant_name()
|
||||
ca_el = find_first_element(settings, ["conditionalAppearance"], SET_NS)
|
||||
if ca_el is not None:
|
||||
clear_container_children(ca_el)
|
||||
print(f'[OK] ConditionalAppearance cleared in variant "{var_name}"')
|
||||
else:
|
||||
print(f'[INFO] No conditionalAppearance section in variant "{var_name}"')
|
||||
|
||||
elif operation == "modify-filter":
|
||||
settings = resolve_variant_settings()
|
||||
var_name = get_variant_name()
|
||||
@@ -2237,6 +2542,69 @@ elif operation == "modify-field":
|
||||
|
||||
print(f'[OK] Field "{field_name}" modified in dataset "{ds_name}"')
|
||||
|
||||
elif operation == "set-field-role":
|
||||
ds_node = resolve_data_set()
|
||||
ds_name = get_data_set_name(ds_node)
|
||||
|
||||
for val in values:
|
||||
s = val.strip()
|
||||
|
||||
flags = []
|
||||
for m in re.finditer(r'@(\w+)', s):
|
||||
flags.append(m.group(1))
|
||||
s = re.sub(r'\s*@\w+', '', s).strip()
|
||||
|
||||
kv = []
|
||||
for m in re.finditer(r'(\w+)=(\S+)', s):
|
||||
kv.append((m.group(1), m.group(2)))
|
||||
s = re.sub(r'\s*\w+=\S+', '', s).strip()
|
||||
|
||||
data_path = s
|
||||
if not data_path:
|
||||
print(f'[WARN] set-field-role: empty dataPath in "{val}"')
|
||||
continue
|
||||
|
||||
field_el = find_element_by_child_value(ds_node, "field", "dataPath", data_path, SCH_NS)
|
||||
if field_el is None:
|
||||
print(f'[WARN] Field "{data_path}" not found in dataset "{ds_name}"')
|
||||
continue
|
||||
|
||||
field_indent = get_child_indent(field_el)
|
||||
|
||||
# Remove existing <role>
|
||||
old_role = next((ch for ch in field_el if isinstance(ch.tag, str) and local_name(ch) == "role" and etree.QName(ch.tag).namespace == SCH_NS), None)
|
||||
if old_role is not None:
|
||||
remove_node_with_whitespace(old_role)
|
||||
|
||||
# Empty spec — remove only
|
||||
if not flags and not kv:
|
||||
print(f'[OK] Field "{data_path}" role cleared')
|
||||
continue
|
||||
|
||||
# Build new <role>
|
||||
lines = [f"{field_indent}<role>"]
|
||||
for flag in flags:
|
||||
if flag == "period":
|
||||
lines.append(f"{field_indent}\t<dcscom:periodNumber>1</dcscom:periodNumber>")
|
||||
lines.append(f"{field_indent}\t<dcscom:periodType>Main</dcscom:periodType>")
|
||||
else:
|
||||
lines.append(f"{field_indent}\t<dcscom:{flag}>true</dcscom:{flag}>")
|
||||
for k, v in kv:
|
||||
lines.append(f"{field_indent}\t<dcscom:{k}>{esc_xml(v)}</dcscom:{k}>")
|
||||
lines.append(f"{field_indent}</role>")
|
||||
frag_xml = "\r\n".join(lines)
|
||||
|
||||
ref_node = next((ch for ch in field_el if isinstance(ch.tag, str) and local_name(ch) in ("valueType", "inputParameters") and etree.QName(ch.tag).namespace == SCH_NS), None)
|
||||
for node in import_fragment(xml_doc, frag_xml):
|
||||
insert_before_element(field_el, node, ref_node, field_indent)
|
||||
|
||||
parts = []
|
||||
if flags:
|
||||
parts.append(" ".join(f"@{f}" for f in flags))
|
||||
if kv:
|
||||
parts.append(" ".join(f"{k}={v}" for k, v in kv))
|
||||
print(f'[OK] Field "{data_path}" role set: {" ".join(parts)}')
|
||||
|
||||
elif operation == "remove-field":
|
||||
ds_node = resolve_data_set()
|
||||
ds_name = get_data_set_name(ds_node)
|
||||
|
||||
@@ -24,7 +24,7 @@ allowed-tools:
|
||||
| `OutFile` | Записать результат в файл (UTF-8 BOM) |
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File .claude/skills/skd-info/scripts/skd-info.ps1 -TemplatePath "<путь>"
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/skd-info.ps1" -TemplatePath "<путь>"
|
||||
```
|
||||
|
||||
С указанием режима:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# skd-info v1.3 — Analyze 1C DCS structure
|
||||
# skd-info v1.4 — Analyze 1C DCS structure
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory=$true)]
|
||||
@@ -824,8 +824,14 @@ function Show-Fields {
|
||||
$roleParts = @()
|
||||
if ($role) {
|
||||
foreach ($child in $role.ChildNodes) {
|
||||
if ($child.NodeType -eq "Element" -and $child.InnerText -eq "true") {
|
||||
if ($child.NodeType -ne "Element") { continue }
|
||||
$txt = $child.InnerText.Trim()
|
||||
if ($txt -eq "true") {
|
||||
$roleParts += $child.LocalName
|
||||
} elseif ($txt -eq "false") {
|
||||
# skip default-false flags
|
||||
} else {
|
||||
$roleParts += "$($child.LocalName)=$txt"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# skd-info v1.3 — Analyze 1C DCS structure
|
||||
# skd-info v1.4 — Analyze 1C DCS structure
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
@@ -777,8 +777,15 @@ def main():
|
||||
role_parts = []
|
||||
if role is not None:
|
||||
for child in role:
|
||||
if isinstance(child.tag, str) and (child.text or "").strip() == "true":
|
||||
if not isinstance(child.tag, str):
|
||||
continue
|
||||
txt = (child.text or "").strip()
|
||||
if txt == "true":
|
||||
role_parts.append(localname(child))
|
||||
elif txt == "false":
|
||||
pass
|
||||
else:
|
||||
role_parts.append(f"{localname(child)}={txt}")
|
||||
info["role"] = ", ".join(role_parts)
|
||||
|
||||
# UseRestriction
|
||||
|
||||
@@ -24,6 +24,6 @@ allowed-tools:
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File .claude/skills/skd-validate/scripts/skd-validate.ps1 -TemplatePath "src/МойОтчёт/Templates/ОсновнаяСхема"
|
||||
powershell.exe -NoProfile -File .claude/skills/skd-validate/scripts/skd-validate.ps1 -TemplatePath "Catalogs/Номенклатура/Templates/СКД/Ext/Template.xml"
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/skd-validate.ps1" -TemplatePath "src/МойОтчёт/Templates/ОсновнаяСхема"
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/skd-validate.ps1" -TemplatePath "Catalogs/Номенклатура/Templates/СКД/Ext/Template.xml"
|
||||
```
|
||||
|
||||
@@ -24,7 +24,7 @@ allowed-tools:
|
||||
| `NoValidate` | Пропустить авто-валидацию |
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File '.claude/skills/subsystem-compile/scripts/subsystem-compile.ps1' -Value '<json>' -OutputDir '<ConfigDir>'
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/subsystem-compile.ps1" -Value '<json>' -OutputDir '<ConfigDir>'
|
||||
```
|
||||
|
||||
## JSON-определение
|
||||
|
||||
@@ -24,7 +24,7 @@ allowed-tools:
|
||||
| `NoValidate` | Пропустить авто-валидацию |
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File '.claude/skills/subsystem-edit/scripts/subsystem-edit.ps1' -SubsystemPath '<path>' -Operation add-content -Value 'Catalog.Товары'
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/subsystem-edit.ps1" -SubsystemPath '<path>' -Operation add-content -Value 'Catalog.Товары'
|
||||
```
|
||||
|
||||
## Операции
|
||||
|
||||
@@ -23,7 +23,7 @@ allowed-tools:
|
||||
| `OutFile` | Записать результат в файл (UTF-8 BOM) |
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File .claude/skills/subsystem-info/scripts/subsystem-info.ps1 -SubsystemPath "<путь>"
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/subsystem-info.ps1" -SubsystemPath "<путь>"
|
||||
```
|
||||
|
||||
## Пять режимов
|
||||
|
||||
@@ -24,6 +24,6 @@ allowed-tools:
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File ".claude/skills/subsystem-validate/scripts/subsystem-validate.ps1" -SubsystemPath "Subsystems/Продажи"
|
||||
powershell.exe -NoProfile -File ".claude/skills/subsystem-validate/scripts/subsystem-validate.ps1" -SubsystemPath "Subsystems/Продажи.xml"
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/subsystem-validate.ps1" -SubsystemPath "Subsystems/Продажи"
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/subsystem-validate.ps1" -SubsystemPath "Subsystems/Продажи.xml"
|
||||
```
|
||||
|
||||
@@ -33,7 +33,7 @@ allowed-tools:
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File .claude/skills/template-add/scripts/add-template.ps1 -ObjectName "<ObjectName>" -TemplateName "<TemplateName>" -TemplateType "<TemplateType>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"] [-SetMainSKD]
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/add-template.ps1" -ObjectName "<ObjectName>" -TemplateName "<TemplateName>" -TemplateType "<TemplateType>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"] [-SetMainSKD]
|
||||
```
|
||||
|
||||
## Пример
|
||||
@@ -41,7 +41,7 @@ powershell.exe -NoProfile -File .claude/skills/template-add/scripts/add-template
|
||||
Добавить основную СКД к отчёту в расширении:
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File .claude/skills/template-add/scripts/add-template.ps1 -ObjectName "ОтчётПродажи" -TemplateName "ОсновнаяСхемаКомпоновкиДанных" -TemplateType "DataCompositionSchema" -SrcDir "src/cfe/МоёРасширение/Reports"
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/add-template.ps1" -ObjectName "ОтчётПродажи" -TemplateName "ОсновнаяСхемаКомпоновкиДанных" -TemplateType "DataCompositionSchema" -SrcDir "src/cfe/МоёРасширение/Reports"
|
||||
```
|
||||
|
||||
## Маппинг типов
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# template-add v1.4 — Add template to 1C object
|
||||
# template-add v1.5 — Add template to 1C object
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
@@ -20,6 +20,8 @@ param(
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
[Console]::InputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# --- Маппинг типов ---
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# add-template v1.4 — Add template to 1C object
|
||||
# add-template v1.5 — Add template to 1C object
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
|
||||
@@ -31,7 +31,7 @@ allowed-tools:
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File .claude/skills/template-remove/scripts/remove-template.ps1 -ObjectName "<ObjectName>" -TemplateName "<TemplateName>" [-SrcDir "<SrcDir>"]
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/remove-template.ps1" -ObjectName "<ObjectName>" -TemplateName "<TemplateName>" [-SrcDir "<SrcDir>"]
|
||||
```
|
||||
|
||||
## Что удаляется
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# template-remove v1.1 — Remove template from 1C object
|
||||
# template-remove v1.2 — Remove template 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-template v1.0 — Remove template from 1C object
|
||||
# remove-template v1.1 — Remove template from 1C object
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
|
||||
@@ -26,7 +26,7 @@ allowed-tools:
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File .claude/skills/web-info/scripts/web-info.ps1 <параметры>
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/web-info.ps1" <параметры>
|
||||
```
|
||||
|
||||
### Параметры скрипта
|
||||
@@ -55,8 +55,8 @@ Module: C:/Program Files/1cv8/8.3.24.1691/bin/wsap24.dll
|
||||
|
||||
```powershell
|
||||
# Статус по умолчанию
|
||||
powershell.exe -NoProfile -File .claude/skills/web-info/scripts/web-info.ps1
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/web-info.ps1"
|
||||
|
||||
# Указать путь к Apache
|
||||
powershell.exe -NoProfile -File .claude/skills/web-info/scripts/web-info.ps1 -ApachePath "C:\tools\apache24"
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/web-info.ps1" -ApachePath "C:\tools\apache24"
|
||||
```
|
||||
|
||||
@@ -41,7 +41,7 @@ allowed-tools:
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File .claude/skills/web-publish/scripts/web-publish.ps1 <параметры>
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/web-publish.ps1" <параметры>
|
||||
```
|
||||
|
||||
### Параметры скрипта
|
||||
@@ -88,14 +88,14 @@ powershell.exe -NoProfile -File .claude/skills/web-publish/scripts/web-publish.p
|
||||
|
||||
```powershell
|
||||
# Файловая база
|
||||
powershell.exe -NoProfile -File .claude/skills/web-publish/scripts/web-publish.ps1 -InfoBasePath "C:\Bases\MyDB" -UserName "Admin"
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/web-publish.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin"
|
||||
|
||||
# С явным именем публикации и портом
|
||||
powershell.exe -NoProfile -File .claude/skills/web-publish/scripts/web-publish.ps1 -InfoBasePath "C:\Bases\MyDB" -AppName "mydb" -Port 9090
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/web-publish.ps1" -InfoBasePath "C:\Bases\MyDB" -AppName "mydb" -Port 9090
|
||||
|
||||
# Серверная база
|
||||
powershell.exe -NoProfile -File .claude/skills/web-publish/scripts/web-publish.ps1 -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret"
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/web-publish.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret"
|
||||
|
||||
# Ручной режим (только инструкция)
|
||||
powershell.exe -NoProfile -File .claude/skills/web-publish/scripts/web-publish.ps1 -InfoBasePath "C:\Bases\MyDB" -Manual
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/web-publish.ps1" -InfoBasePath "C:\Bases\MyDB" -Manual
|
||||
```
|
||||
|
||||
@@ -26,7 +26,7 @@ allowed-tools:
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File .claude/skills/web-stop/scripts/web-stop.ps1 <параметры>
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/web-stop.ps1" <параметры>
|
||||
```
|
||||
|
||||
### Параметры скрипта
|
||||
@@ -45,8 +45,8 @@ powershell.exe -NoProfile -File .claude/skills/web-stop/scripts/web-stop.ps1 <п
|
||||
|
||||
```powershell
|
||||
# Остановить Apache
|
||||
powershell.exe -NoProfile -File .claude/skills/web-stop/scripts/web-stop.ps1
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/web-stop.ps1"
|
||||
|
||||
# С указанием пути
|
||||
powershell.exe -NoProfile -File .claude/skills/web-stop/scripts/web-stop.ps1 -ApachePath "C:\tools\apache24"
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/web-stop.ps1" -ApachePath "C:\tools\apache24"
|
||||
```
|
||||
|
||||
@@ -17,7 +17,7 @@ Automates user interactions with 1C:Enterprise web client via Playwright — nav
|
||||
## Quick start
|
||||
|
||||
```bash
|
||||
RUN=".claude/skills/web-test/scripts/run.mjs"
|
||||
RUN="${CLAUDE_SKILL_DIR}/scripts/run.mjs"
|
||||
|
||||
# One-shot: opens browser → runs script → closes browser → exits
|
||||
node $RUN run http://localhost:8081/bpdemo test-scenario.js
|
||||
@@ -35,7 +35,7 @@ SCRIPT
|
||||
## Setup (first time)
|
||||
|
||||
```bash
|
||||
cd .claude/skills/web-test/scripts && npm install
|
||||
cd "${CLAUDE_SKILL_DIR}/scripts" && npm install
|
||||
```
|
||||
|
||||
Requires Node.js 18+. `npm install` downloads Playwright and Chromium.
|
||||
@@ -529,3 +529,7 @@ On error (auto-screenshot taken):
|
||||
- **Cyrillic in bash** — use `cat <<'SCRIPT' | node $RUN exec -` to avoid escaping issues
|
||||
- **Non-breaking spaces** — 1C uses `\u00a0` instead of regular spaces. All matching is normalized internally
|
||||
- **Section panel display** — `navigateSection()` works with any panel position (side, top) but requires "Picture and text" or "Text" display mode. Icon-only mode is not supported — API cannot read section names from icons alone
|
||||
|
||||
## Regression suites
|
||||
|
||||
When the user asks to cover a 1C solution with automated regression — multi-file test suites with assertions, hooks, tags, retries, Allure/JUnit reports, multi-user process tests — switch to the `test` mode. See [regress.md](regress.md) for authoring discipline, recon flow (metadata + live walkthrough via `exec`), per-application folder layout, ready-to-paste templates, and failure triage. Default to ad-hoc `run`/`exec` for single-script automation — `test` is the specialised mode for project-wide coverage.
|
||||
|
||||
@@ -0,0 +1,433 @@
|
||||
# Regression suite authoring
|
||||
|
||||
Use this when the user asks to cover a 1C solution with automated regression tests, build out a test suite, or run an existing suite and analyse failures. For ad-hoc single-script automation, stay with the `run`/`exec` modes from SKILL.md instead.
|
||||
|
||||
The runner is the same `run.mjs`. The mode is `test`:
|
||||
|
||||
```bash
|
||||
node $RUN test [url] <dir|file> [flags]
|
||||
```
|
||||
|
||||
Tests live next to the project they cover (not inside the skill). Convention: `tests/` at the project root, with `_hooks.mjs` and `webtest.config.mjs` at the suite root. Tests are ES modules with `*.test.mjs` suffix.
|
||||
|
||||
## When to choose `test` over `exec`
|
||||
|
||||
| Goal | Mode |
|
||||
|------|------|
|
||||
| Explore a form, prototype a single step, debug one selector | `exec` (interactive session) |
|
||||
| **Walk through a scenario live before committing it as a test** | `exec` first, then `test` |
|
||||
| Reproduce a bug as a failing test before fixing it | `test` |
|
||||
| Cover a feature so future changes are checked automatically | `test` |
|
||||
| Run the project's regression on a new build | `test` |
|
||||
| Generate a screencast walkthrough | `exec` with `startRecording` |
|
||||
|
||||
Don't write a `.test.mjs` for a one-shot user request. Don't drive a regression suite through chained `exec` calls.
|
||||
|
||||
## Before writing tests — recon
|
||||
|
||||
Two layers, in order. Don't skip either.
|
||||
|
||||
### 1. Static recon — metadata
|
||||
|
||||
Never invent identifiers. For every metadata object the user mentions (or that you decide to cover), run the matching info skill first:
|
||||
|
||||
| Object type | Skill |
|
||||
|-------------|-------|
|
||||
| Catalog/document/register attributes, tabular sections | `/meta-info` |
|
||||
| Form layout — fields, buttons, tabs, tables | `/form-info` |
|
||||
| DCS report — fields, parameters, filters | `/skd-info` |
|
||||
| Spreadsheet template areas/parameters | `/mxl-info` |
|
||||
| Role rights / restrictions | `/role-info` |
|
||||
| Subsystem composition / command interface | `/subsystem-info` |
|
||||
|
||||
This gives the real Russian field labels, command names, column headers, table-section names. Without it, fuzzy matching will silently land on the wrong element, or fail with no useful diagnostic.
|
||||
|
||||
If the user names objects you cannot find: stop and ask. Do not guess.
|
||||
|
||||
### 2. Live recon — interactive walkthrough
|
||||
|
||||
For any non-trivial scenario, walk the path live in `exec` mode before writing it down. Metadata tells you what exists; the live walkthrough tells you what actually happens — which button posts the document, which dialog 1C raises, how the form looks after `clickElement('Создать')`, what fields are required, where `wait()` is genuinely needed.
|
||||
|
||||
```bash
|
||||
# Start a session (background).
|
||||
node $RUN start http://localhost:9191/myapp/ru_RU
|
||||
|
||||
# Step the scenario interactively. After each step, inspect.
|
||||
cat <<'EOF' | node $RUN exec -
|
||||
await navigateSection('Склад');
|
||||
const cmds = await getCommands();
|
||||
console.log(cmds);
|
||||
EOF
|
||||
|
||||
cat <<'EOF' | node $RUN exec -
|
||||
await openCommand('Приходная накладная');
|
||||
await clickElement('Создать');
|
||||
const s = await getFormState();
|
||||
console.log(JSON.stringify(s.fields.map(f => ({ name: f.name, label: f.label, required: f.required })), null, 2));
|
||||
console.log('buttons:', s.buttons.map(b => b.name));
|
||||
console.log('tables:', s.tables.map(t => ({ name: t.name, label: t.label, columns: t.columns })));
|
||||
EOF
|
||||
|
||||
# Try the actions you plan to encode. If a step fails, fix and re-try
|
||||
# before transcribing it.
|
||||
cat <<'EOF' | node $RUN exec -
|
||||
await fillFields({ 'Контрагент': 'ООО Север' });
|
||||
await fillTableRow({ 'Номенклатура': 'Товар 01', 'Количество': '5' },
|
||||
{ table: 'Товары', add: true });
|
||||
await clickElement('Провести и закрыть');
|
||||
console.log(JSON.stringify(await getFormState()));
|
||||
EOF
|
||||
|
||||
# When done, stop the session (or leave it for the next test you write).
|
||||
node $RUN stop
|
||||
```
|
||||
|
||||
What to record from the walkthrough into the test:
|
||||
- Exact button names (`'Провести и закрыть'`, not `'Сохранить'`).
|
||||
- Field labels as 1C renders them (with possible non-breaking spaces — `fillFields` normalises, but be exact).
|
||||
- Table section names from `getFormState().tables[].name`/`label` for multi-grid forms.
|
||||
- Required `wait()` durations — only where a real async event happens (report generation, server-side calculation). Default actions await internally.
|
||||
- The shape of `getFormState()` after each action — gives you the right `assert.equal(...)` paths.
|
||||
|
||||
After this, transcribe the working sequence into `*.test.mjs`, wrap each chunk in `step('...', async () => { ... })`, add assertions for the invariants you saw. Run the file once with `node $RUN test path/to/file.test.mjs` to confirm.
|
||||
|
||||
When live recon is overkill: trivial reads (`navigateSection` + `readTable` + assert non-empty), or scenarios you've already proven once in this session. When it's essential: anything with confirmation dialogs, posting/cancellation flows, reports with custom filters, multi-grid forms, or user-customised forms you've never seen.
|
||||
|
||||
## Suite layout
|
||||
|
||||
**Each application gets its own subfolder under `tests/`.** A single repo may host several independent suites side by side — they must not share `_hooks.mjs` or `webtest.config.mjs`, because each suite restores a different DB, publishes to a different URL, and ships its own test data.
|
||||
|
||||
```
|
||||
tests/
|
||||
web-test/ # engine self-tests (reserved if our repo layout)
|
||||
<app-name>/ # application regression — one per solution
|
||||
_hooks.mjs
|
||||
webtest.config.mjs
|
||||
01-login/
|
||||
02-counterparties/
|
||||
...
|
||||
<another-app>/ # second solution, fully isolated
|
||||
_hooks.mjs
|
||||
...
|
||||
```
|
||||
|
||||
`<app-name>` is the project/extension slug (`acc-payroll`, `erp-customisation`, etc.). Pick something stable and pass it on the CLI:
|
||||
|
||||
```bash
|
||||
node $RUN test tests/<app-name>/
|
||||
```
|
||||
|
||||
Inside the application subfolder, organize by **feature**, not by metadata kind. Numeric prefixes on both folder and file enforce run order (discovery is alphabetic by full path).
|
||||
|
||||
```
|
||||
tests/<app-name>/
|
||||
_hooks.mjs # stand prep + cross-cutting hooks (optional)
|
||||
webtest.config.mjs # url, contexts, defaults (optional)
|
||||
01-login/
|
||||
01-open-base.test.mjs
|
||||
02-section-navigation.test.mjs
|
||||
02-counterparties/
|
||||
01-create.test.mjs
|
||||
02-edit-phone.test.mjs
|
||||
03-goods-receipt/
|
||||
01-fill.test.mjs
|
||||
02-post.test.mjs
|
||||
03-unpost.test.mjs
|
||||
04-balance-report/
|
||||
01-generate.test.mjs
|
||||
02-warehouse-filter.test.mjs
|
||||
05-approval-process/
|
||||
01-end-to-end.test.mjs # multi-user
|
||||
```
|
||||
|
||||
Per-folder `_hooks.mjs` / `webtest.config.mjs` inside the application subfolder are NOT supported. Only the application-root copies are loaded.
|
||||
|
||||
## Test file anatomy
|
||||
|
||||
```js
|
||||
export const name = 'Создание контрагента'; // required
|
||||
export const tags = ['catalog', 'create']; // optional, used for filtering + Allure
|
||||
export const timeout = 60000; // optional, default 30000
|
||||
// export const skip = 'pending fix #123'; // optional: true | string
|
||||
// export const only = true; // debug-only — never commit
|
||||
// export const context = 'manager'; // optional, single non-default context
|
||||
// export const contexts = ['clerk', 'manager']; // optional, multi-user test
|
||||
// export const severity = 'critical'; // optional, overrides config severity
|
||||
|
||||
export async function setup(ctx) {
|
||||
// per-test prep — runs before default. Skip if not needed.
|
||||
}
|
||||
|
||||
export async function teardown(ctx) {
|
||||
// per-test cleanup — runs after default, always (even on failure).
|
||||
}
|
||||
|
||||
export default async function(ctx) {
|
||||
const { navigateSection, openCommand, clickElement, fillFields,
|
||||
readTable, closeForm, getFormState,
|
||||
assert, step, log } = ctx;
|
||||
|
||||
await step('Открыть список контрагентов', async () => {
|
||||
await navigateSection('Продажи');
|
||||
await openCommand('Контрагенты');
|
||||
});
|
||||
|
||||
await step('Создать нового контрагента', async () => {
|
||||
await clickElement('Создать');
|
||||
await fillFields({ 'Наименование': 'Тест ' + Date.now() });
|
||||
await clickElement('Записать и закрыть');
|
||||
});
|
||||
|
||||
await step('Убедиться, что элемент появился в списке', async () => {
|
||||
const t = await readTable();
|
||||
assert.tableHasRow(t, r => r['Наименование']?.startsWith('Тест '));
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
The runner injects every `browser.mjs` export into `ctx` plus `assert`, `step`, `log`, `testInfo`, `testResult` (afterEach only). For multi-context tests, each context name is its own scoped namespace (`ctx.clerk.clickElement(...)` etc.) — `step`/`assert` stay top-level.
|
||||
|
||||
**Step names — in Russian, descriptive.** Step labels surface in the console output, in JSON/JUnit, and as Allure step nodes. Russian-speaking QA reads them. Use a full action phrase (`'Создать нового контрагента'`, `'Проверить наличие документа в списке'`), not a tag (`'create'`, `'verify'`) and not a transliteration. Same applies to `export const name` and `displayName` in `webtest.config.mjs`.
|
||||
|
||||
## webtest.config.mjs
|
||||
|
||||
```js
|
||||
export default {
|
||||
// Single-context: just url.
|
||||
url: 'http://localhost:9191/myapp/ru_RU',
|
||||
|
||||
// OR multi-context: named contexts. Each test picks via `context`/`contexts` exports.
|
||||
// contexts: {
|
||||
// clerk: { url: 'http://localhost:9191/myapp-clerk/ru_RU', displayName: 'Кладовщик' },
|
||||
// manager: { url: 'http://localhost:9191/myapp-manager/ru_RU', displayName: 'Менеджер' },
|
||||
// },
|
||||
// defaultContext: 'clerk',
|
||||
|
||||
timeout: 30000,
|
||||
retries: 0,
|
||||
screenshot: 'on-failure',
|
||||
record: false,
|
||||
|
||||
// Severity → tags mapping for Allure. Each tag at most one bucket.
|
||||
severity: {
|
||||
critical: ['smoke', 'crud'],
|
||||
minor: ['recording'],
|
||||
},
|
||||
defaultSeverity: 'normal',
|
||||
};
|
||||
```
|
||||
|
||||
CLI flags override config. Recommend latin context IDs + Russian `displayName` for video badges.
|
||||
|
||||
## _hooks.mjs
|
||||
|
||||
Two layers. Infra hooks run without a browser; testlevel hooks receive `ctx`.
|
||||
|
||||
```js
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
// Infra — runs once around the whole suite.
|
||||
export async function prepare({ hookArgs, log, config }) {
|
||||
// Restore DB, publish to Apache, build EPF, etc.
|
||||
// hookArgs = everything after `--` on the CLI. Parse yourself.
|
||||
if (hookArgs.includes('--rebuild-stand')) { /* full rebuild */ }
|
||||
// Use idempotent hash-locks to skip work on warm starts.
|
||||
}
|
||||
|
||||
export async function cleanup({ log, config }) {
|
||||
// Tear down or leave the stand running. Choose per project.
|
||||
}
|
||||
|
||||
// Testlevel — runs with browser ctx.
|
||||
export async function beforeAll(ctx) { /* once after first context opens */ }
|
||||
export async function afterAll(ctx) { /* once before final teardown */ }
|
||||
export async function beforeEach(ctx) { /* ctx.testInfo is set */ }
|
||||
export async function afterEach(ctx) { /* ctx.testResult is set */ }
|
||||
|
||||
// Per-context — runs whenever a context is created/closed.
|
||||
export async function afterOpenContext(ctx, name, spec) { /* spec = config.contexts[name] */ }
|
||||
export async function beforeCloseContext(ctx, name, spec) { }
|
||||
```
|
||||
|
||||
Built-in state reset (`dismissPendingErrors` + close all forms) runs after `afterEach` automatically. Don't reimplement it.
|
||||
|
||||
**Where to put data setup:**
|
||||
- DB restore, publication, EPF build → `prepare()`. Make it idempotent (hash-locks on inputs — config sources, EPF spec, DB dump) so warm starts skip everything but a liveness probe.
|
||||
- Test-specific seed data (the document this test will edit, the counterparty it expects) → per-test `setup`.
|
||||
- Shared session-wide warmup → `beforeAll`.
|
||||
|
||||
## Ready-to-paste patterns
|
||||
|
||||
### Catalog full cycle
|
||||
|
||||
```js
|
||||
await step('Создать контрагента', async () => {
|
||||
await navigateSection('Продажи');
|
||||
await openCommand('Контрагенты');
|
||||
await clickElement('Создать');
|
||||
await fillFields({ 'Наименование': 'ТД Тест', 'ИНН': '7707083893' });
|
||||
await clickElement('Записать и закрыть');
|
||||
});
|
||||
await step('Проверить наличие в списке', async () => {
|
||||
const t = await readTable({ maxRows: 50 });
|
||||
assert.tableHasRow(t, { 'Наименование': 'ТД Тест' });
|
||||
});
|
||||
await step('Удалить контрагента и подтвердить удаление', async () => {
|
||||
await clickElement('ТД Тест');
|
||||
const page = await getPage();
|
||||
await page.keyboard.press('Delete');
|
||||
await clickElement('Да');
|
||||
});
|
||||
```
|
||||
|
||||
### Document create + post
|
||||
|
||||
```js
|
||||
const marker = 'Тест-' + Date.now();
|
||||
await openCommand('Приходная накладная');
|
||||
await clickElement('Создать');
|
||||
await fillFields({ 'Контрагент': 'ООО Север', 'Комментарий': marker });
|
||||
await fillTableRow(
|
||||
{ 'Номенклатура': 'Товар 01', 'Количество': '5', 'Цена': '100' },
|
||||
{ table: 'Товары', add: true }
|
||||
);
|
||||
await clickElement('Провести и закрыть');
|
||||
// Verify: re-open list, filter or scan, assert by `marker`.
|
||||
```
|
||||
|
||||
Use a unique marker (`Date.now()` or random suffix) so re-runs don't collide. Identify your own row by it, not by position or natural keys that may already exist in the DB.
|
||||
|
||||
### DCS report
|
||||
|
||||
```js
|
||||
await openCommand('Остатки товаров');
|
||||
// Reset user settings — 1C persists them between sessions.
|
||||
await clickElement('Ещё');
|
||||
await clickElement('Установить стандартные настройки');
|
||||
|
||||
await selectValue('Номенклатура', 'Товар 02'); // auto-enables the filter checkbox
|
||||
await clickElement('Сформировать');
|
||||
await wait(3);
|
||||
const r = await readSpreadsheet();
|
||||
assert.deepEqual(r.headers, ['Номенклатура', 'Количество', 'Сумма']);
|
||||
assert.ok(r.data.length >= 1);
|
||||
assert.ok(r.totals?.['Сумма']);
|
||||
```
|
||||
|
||||
### Multi-user process
|
||||
|
||||
```js
|
||||
export const contexts = ['clerk', 'manager'];
|
||||
|
||||
export default async function({ clerk, manager, step, assert }) {
|
||||
await step('Кладовщик создаёт накладную', async () => {
|
||||
await clerk.navigateSection('Склад');
|
||||
await clerk.openCommand('Приходные накладные');
|
||||
await clerk.clickElement('Создать');
|
||||
await clerk.fillFields({ 'Контрагент': 'ООО Север' });
|
||||
await clerk.clickElement('Записать');
|
||||
});
|
||||
await step('Менеджер утверждает накладную', async () => {
|
||||
await manager.navigateSection('Согласование');
|
||||
await manager.openCommand('На утверждении');
|
||||
await manager.clickElement('ООО Север', { dblclick: true });
|
||||
await manager.clickElement('Утвердить');
|
||||
});
|
||||
await step('Кладовщик видит новый статус', async () => {
|
||||
const s = await clerk.getFormState();
|
||||
assert.equal(s.fields.find(f => f.name === 'Статус')?.value, 'Утверждён');
|
||||
});
|
||||
await step('Освободить сессию кладовщика', async () => {
|
||||
await manager.closeContext('clerk'); // free a 1C license for the next test
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
License caveat: stock 1C allows ~2 web sessions concurrently. Close contexts you no longer need before the next multi-user test starts.
|
||||
|
||||
### Failing-test repro
|
||||
|
||||
```js
|
||||
export const name = 'Bug #123: накладная без контрагента не должна проводиться';
|
||||
export const tags = ['bug', 'validation'];
|
||||
|
||||
export default async function({ openCommand, clickElement, getFormState, assert, step }) {
|
||||
await openCommand('Приходные накладные');
|
||||
await clickElement('Создать');
|
||||
await clickElement('Провести');
|
||||
const s = await getFormState();
|
||||
assert.ok(s.errorModal || s.fields.find(f => f.name === 'Контрагент')?.required,
|
||||
'Должна быть ошибка валидации или поле помечено обязательным');
|
||||
}
|
||||
```
|
||||
|
||||
Write it red first, hand it to the user, fix the underlying issue, re-run green.
|
||||
|
||||
## Running
|
||||
|
||||
```bash
|
||||
node $RUN test tests/<app-name>/ # full app suite
|
||||
node $RUN test tests/<app-name>/03-goods-receipt/ # one feature folder
|
||||
node $RUN test tests/<app-name>/02-counterparties/01-create.test.mjs # one file
|
||||
node $RUN test tests/<app-name>/ --tags=smoke # by tag (intersection)
|
||||
node $RUN test tests/<app-name>/ --grep='накладн' # by name regex
|
||||
node $RUN test tests/<app-name>/ --bail --retry=1 # stop on first fail, allow 1 retry
|
||||
node $RUN test tests/<app-name>/ --report=allure-results --format=allure --report-dir=allure-results
|
||||
node $RUN test tests/<app-name>/ -- --rebuild-stand # everything after `--` goes to hooks
|
||||
```
|
||||
|
||||
Default report is JSON when `--report=…` is given. Allure needs `--format=allure` + a directory. JUnit similarly with `--format=junit`.
|
||||
|
||||
### Allure static config — `_allure/` directory
|
||||
|
||||
The runner copies `<testDir>/_allure/` into the report directory before generating Allure output. Standard Allure convention applies — three files are typically used:
|
||||
|
||||
- **`categories.json`** — failure classification. Always emit this when setting up a suite, with 1C-specific patterns: license pool exhaustion (`Не обнаружено свободной лицензии`), 1C application errors (`ВызватьИсключение|Произошла ошибка|…`), navigation/element lookup misses, runner timeouts, assertion failures.
|
||||
- **`environment.properties`** — `key=value` lines for the Environment widget. Useful when the suite runs across builds/branches (URL, 1C platform version, git branch, configuration version). Often emitted dynamically by `prepare()` rather than committed as a static file.
|
||||
- **`executor.json`** — CI metadata (Jenkins URL, GitHub run ID, etc.). Only relevant when the suite runs on a CI server; for local runs, skip it.
|
||||
|
||||
Discovery skips the underscored directory, so it never collides with tests.
|
||||
|
||||
## Severity guidance
|
||||
|
||||
When the user doesn't dictate, default to:
|
||||
|
||||
| Test kind | Severity |
|
||||
|-----------|----------|
|
||||
| Login + section navigation, basic CRUD on covered entities | `critical` (also tag `smoke`) |
|
||||
| Documents posting, report generation, end-to-end processes | `critical` |
|
||||
| Field-level edge cases, formatting, optional flows | `normal` |
|
||||
| Cosmetic / recording / non-functional | `minor` |
|
||||
| Reserved for show-stopper protections | `blocker` (use sparingly) |
|
||||
|
||||
Don't promote everything to `critical` — it loses signal in the Allure dashboard.
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- **Sleeps as a substitute for assertions.** `wait(5)` after `openCommand` is fine; `wait(30)` because something flakes is a bug — find what state you can wait on with `getFormState` instead.
|
||||
- **Retry as a substitute for understanding.** "Not found" twice means the data isn't there or the label is wrong. Don't loop.
|
||||
- **Raw DOM via `getPage().$$(...)`.** Use `getFormState`, `readTable`, `readSpreadsheet`. Raw selectors break across 1C platform versions.
|
||||
- **`clickElement('×')` or `clickElement('Закрыть')`** to dismiss a form. Use `closeForm({ save: true|false })` — handles confirmation correctly.
|
||||
- **Position-based row identification** (`rows[0]`) when the DB has shared seed data. Filter by unique marker or label instead.
|
||||
- **Skipping recon** because "I know what this catalog looks like." You don't — the project's customisation almost certainly differs from a stock config.
|
||||
- **`tags: ['smoke']` on a 90-second test.** Smoke means fast.
|
||||
- **Hand-writing reset code** in `afterEach`. The runner already closes forms and dismisses errors.
|
||||
- **Cross-test state assumptions.** Each test must start from desktop and seed its own data. Order-of-execution coupling is a regression-suite trap.
|
||||
|
||||
## After a run — failure triage
|
||||
|
||||
1. Scan the JSON or Allure summary for `failed`.
|
||||
2. For each failure, read `error.message` + `error.step` + screenshot (saved next to the report).
|
||||
3. If `error.onecError.stack` is present — it's a 1C exception, look at the platform trace.
|
||||
4. Classify:
|
||||
- **Test bug** — selector wrong, expectation wrong, race with no anchor → fix the test.
|
||||
- **Application bug** — actual misbehaviour reproduced → report to the user with the failing step name and the platform stack.
|
||||
- **Stand flake** — Apache timeout, login form not loading, license shortage → fix the hook idempotency or session-cleanup logic, not the test.
|
||||
5. After fixes, re-run only the affected files (`node $RUN test tests/03-goods-receipt/`) before the full suite.
|
||||
|
||||
Report back to the user with the classification, not raw failure dumps.
|
||||
|
||||
## Reference
|
||||
|
||||
- Browser API: [SKILL.md](SKILL.md)
|
||||
- Video and narration: [recording.md](recording.md)
|
||||
@@ -1,4 +1,4 @@
|
||||
// web-test browser v1.9 — Playwright browser management for 1C web client
|
||||
// web-test browser v1.12 — Playwright browser management for 1C web client
|
||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
/**
|
||||
* Playwright browser management for 1C web client.
|
||||
@@ -37,6 +37,16 @@ let lastCaptions = []; // captions from the last completed recording (for addNar
|
||||
let lastRecordingDuration = null; // wall-clock duration of the last recording (seconds)
|
||||
let highlightMode = false;
|
||||
|
||||
// Multi-context registry: name → { context, page, sessionPrefix, seanceId, recorder, lastCaptions, lastRecordingDuration, highlightMode }
|
||||
// Populated by createContext(); module-level vars above mirror the active slot.
|
||||
// connect() does NOT use this Map — it preserves legacy single-session behavior for exec/run/start.
|
||||
const contexts = new Map();
|
||||
let activeContextName = null;
|
||||
// Isolation mode for the current cmdTest session — set by the first createContext call.
|
||||
// 'tab': all contexts share one persistent context (one window, multiple tabs, extension loads reliably).
|
||||
// 'window': each context gets its own BrowserContext (separate window per context, full cookie isolation, extension may not load).
|
||||
let activeMode = null;
|
||||
|
||||
const LOAD_TIMEOUT = 60000;
|
||||
const INIT_TIMEOUT = 60000;
|
||||
const ACTION_WAIT = 2000; // fallback minimum wait
|
||||
@@ -158,31 +168,51 @@ export async function connect(url, { extensionPath } = {}) {
|
||||
return await getPageState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Best-effort POST /e1cib/logout on a slot to release the 1C session license.
|
||||
* Silent — if page is closed or session info missing, just returns.
|
||||
* @param {object} slot { page, sessionPrefix, seanceId } from contexts Map
|
||||
* @param {number} [waitMs=500] pause after logout fetch (gives 1C time to process)
|
||||
*/
|
||||
async function _logoutSlot(slot, waitMs = 500) {
|
||||
if (!slot?.page || slot.page.isClosed() || !slot.seanceId || !slot.sessionPrefix) return;
|
||||
try {
|
||||
const logoutUrl = `${slot.sessionPrefix}/e1cib/logout?seanceId=${slot.seanceId}`;
|
||||
await slot.page.evaluate(async (url) => {
|
||||
await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{"root":{}}' });
|
||||
}, logoutUrl);
|
||||
await slot.page.waitForTimeout(waitMs);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gracefully terminate the 1C session and close the browser.
|
||||
* Sends POST /e1cib/logout to release the license before closing.
|
||||
*/
|
||||
export async function disconnect() {
|
||||
// Auto-stop recording if active (prevents orphaned ffmpeg)
|
||||
// Multi-context path: stop recording + logout each slot before closing browser
|
||||
if (contexts.size > 0) {
|
||||
_saveActiveSlot();
|
||||
// Recorder is global — one stop covers all contexts
|
||||
if (recorder) {
|
||||
try { await stopRecording(); } catch {}
|
||||
}
|
||||
for (const [, slot] of contexts.entries()) {
|
||||
await _logoutSlot(slot);
|
||||
}
|
||||
contexts.clear();
|
||||
activeContextName = null;
|
||||
activeMode = null;
|
||||
}
|
||||
|
||||
// Single-session path (connect): auto-stop recording if active
|
||||
if (recorder) {
|
||||
try { await stopRecording(); } catch {}
|
||||
}
|
||||
|
||||
if (browser) {
|
||||
// Graceful logout — release the 1C license
|
||||
if (page && !page.isClosed() && seanceId && sessionPrefix) {
|
||||
try {
|
||||
const logoutUrl = `${sessionPrefix}/e1cib/logout?seanceId=${seanceId}`;
|
||||
await page.evaluate(async (url) => {
|
||||
await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: '{"root":{}}'
|
||||
});
|
||||
}, logoutUrl);
|
||||
await page.waitForTimeout(1000);
|
||||
} catch {}
|
||||
}
|
||||
// Graceful logout — release the 1C license (single-session connect path)
|
||||
await _logoutSlot({ page, sessionPrefix, seanceId }, 1000);
|
||||
await browser.close().catch(() => {});
|
||||
browser = null;
|
||||
page = null;
|
||||
@@ -228,6 +258,203 @@ export function getSession() {
|
||||
return { sessionPrefix, seanceId };
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Multi-context support (used by run.mjs cmdTest only)
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Save current module-level state into the active slot before switching.
|
||||
* No-op if no active slot.
|
||||
*/
|
||||
function _saveActiveSlot() {
|
||||
if (!activeContextName) return;
|
||||
const slot = contexts.get(activeContextName);
|
||||
if (!slot) return;
|
||||
slot.page = page;
|
||||
slot.sessionPrefix = sessionPrefix;
|
||||
slot.seanceId = seanceId;
|
||||
slot.highlightMode = highlightMode;
|
||||
// Note: `recorder`, `lastCaptions`, `lastRecordingDuration` are intentionally NOT
|
||||
// mirrored per-slot. A multi-context recording produces one continuous output file —
|
||||
// the recorder follows the active page via recorder._attachPage(), not per-slot state.
|
||||
}
|
||||
|
||||
/** Load a slot's state into module-level vars and mark it active. */
|
||||
function _activateSlot(name) {
|
||||
const slot = contexts.get(name);
|
||||
if (!slot) throw new Error(`Context "${name}" not found. Create it via createContext() first.`);
|
||||
page = slot.page;
|
||||
sessionPrefix = slot.sessionPrefix;
|
||||
seanceId = slot.seanceId;
|
||||
highlightMode = slot.highlightMode || false;
|
||||
activeContextName = name;
|
||||
}
|
||||
|
||||
/** Attach 1C session listeners to a page, writing into the given slot. */
|
||||
function _attachSessionListeners(pg, slot, name) {
|
||||
pg.on('dialog', dialog => dialog.accept().catch(() => {}));
|
||||
pg.on('request', req => {
|
||||
if (slot.seanceId) return;
|
||||
const m = req.url().match(/^(https?:\/\/[^/]+\/[^/]+\/[^/]+)\/e1cib\/.+[?&]seanceId=([^&]+)/);
|
||||
if (m) {
|
||||
slot.sessionPrefix = m[1];
|
||||
slot.seanceId = m[2];
|
||||
if (activeContextName === name) {
|
||||
sessionPrefix = m[1];
|
||||
seanceId = m[2];
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create (or navigate) a named browser context.
|
||||
* First call launches Chromium via chromium.launch() (NOT launchPersistentContext) so that
|
||||
* subsequent calls can create additional isolated BrowserContexts in the same process.
|
||||
* Trade-off: 1C browser extension is loaded via --load-extension (process-level) rather than
|
||||
* persistent profile.
|
||||
*
|
||||
* Use this from run.mjs cmdTest only — exec/run/start use connect() and stay on the
|
||||
* legacy persistent-context path.
|
||||
*/
|
||||
export async function createContext(name, url, { extensionPath, isolation = 'tab' } = {}) {
|
||||
if (contexts.has(name)) {
|
||||
await setActiveContext(name);
|
||||
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: LOAD_TIMEOUT });
|
||||
try { await page.waitForSelector('#themesCell_theme_0', { timeout: INIT_TIMEOUT }); }
|
||||
catch { await page.waitForTimeout(5000); }
|
||||
await closeModals();
|
||||
return await getPageState();
|
||||
}
|
||||
|
||||
if (!['tab', 'window'].includes(isolation)) {
|
||||
throw new Error(`createContext: invalid isolation "${isolation}", expected 'tab' or 'window'`);
|
||||
}
|
||||
if (activeMode && activeMode !== isolation) {
|
||||
throw new Error(`createContext: cannot mix isolation modes — first context used "${activeMode}", "${name}" requested "${isolation}". Use the same mode for all contexts in one run.`);
|
||||
}
|
||||
|
||||
// First context: launch browser. Subsequent: reuse existing.
|
||||
let isFirstContext = !browser;
|
||||
if (isFirstContext) {
|
||||
const extPath = findExtension(extensionPath);
|
||||
const launchArgs = ['--start-maximized'];
|
||||
if (extPath) {
|
||||
launchArgs.push('--disable-extensions-except=' + extPath, '--load-extension=' + extPath);
|
||||
}
|
||||
if (isolation === 'tab') {
|
||||
// Persistent context: extension loads reliably, one window with tabs per context
|
||||
persistentUserDataDir = pathJoin(tmpdir(), 'pw-1c-test-' + Date.now());
|
||||
mkdirSync(persistentUserDataDir, { recursive: true });
|
||||
browser = await chromium.launchPersistentContext(persistentUserDataDir, {
|
||||
headless: false,
|
||||
args: launchArgs,
|
||||
viewport: null,
|
||||
permissions: ['clipboard-read', 'clipboard-write'],
|
||||
});
|
||||
} else {
|
||||
// Window mode: separate BrowserContext per slot, full cookie isolation
|
||||
browser = await chromium.launch({ headless: false, args: launchArgs });
|
||||
}
|
||||
activeMode = isolation;
|
||||
}
|
||||
|
||||
// Save current active before switching
|
||||
_saveActiveSlot();
|
||||
|
||||
// Create slot — page differs by mode
|
||||
let newCtx, newPage;
|
||||
if (activeMode === 'tab') {
|
||||
// Reuse the persistent context for all slots; each slot gets its own page (tab)
|
||||
newCtx = browser;
|
||||
if (isFirstContext) {
|
||||
newPage = browser.pages()[0] || await browser.newPage();
|
||||
} else {
|
||||
newPage = await browser.newPage();
|
||||
}
|
||||
} else {
|
||||
// Window mode: each slot owns its BrowserContext + page
|
||||
newCtx = await browser.newContext({
|
||||
viewport: null,
|
||||
permissions: ['clipboard-read', 'clipboard-write'],
|
||||
});
|
||||
newPage = await newCtx.newPage();
|
||||
}
|
||||
|
||||
const slot = {
|
||||
context: newCtx,
|
||||
page: newPage,
|
||||
sessionPrefix: null,
|
||||
seanceId: null,
|
||||
highlightMode: false,
|
||||
};
|
||||
contexts.set(name, slot);
|
||||
|
||||
_attachSessionListeners(newPage, slot, name);
|
||||
_activateSlot(name);
|
||||
|
||||
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: LOAD_TIMEOUT });
|
||||
try { await page.waitForSelector('#themesCell_theme_0', { timeout: INIT_TIMEOUT }); }
|
||||
catch { await page.waitForTimeout(5000); }
|
||||
await closeModals();
|
||||
|
||||
return await getPageState();
|
||||
}
|
||||
|
||||
/** Switch the active context. Subsequent browser API calls operate on this context's page. */
|
||||
export async function setActiveContext(name) {
|
||||
if (activeContextName === name) return;
|
||||
if (!contexts.has(name)) throw new Error(`Context "${name}" not found. Available: [${[...contexts.keys()].join(', ')}]`);
|
||||
// If a recording is active, flush the outgoing page's last frame so the gap is filled
|
||||
// up to the moment of the switch (avoids a "jump" in video time).
|
||||
if (recorder && recorder._flushFrames) recorder._flushFrames();
|
||||
_saveActiveSlot();
|
||||
_activateSlot(name);
|
||||
// If the recording is still alive (it lives across slots — we keep the same ffmpeg/output),
|
||||
// re-attach its screencast to the newly active page.
|
||||
if (recorder && recorder._attachPage) {
|
||||
await recorder._attachPage(page);
|
||||
}
|
||||
}
|
||||
|
||||
export function listContexts() {
|
||||
return [...contexts.keys()];
|
||||
}
|
||||
|
||||
export function getActiveContext() {
|
||||
return activeContextName;
|
||||
}
|
||||
|
||||
export function hasContext(name) {
|
||||
return contexts.has(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Close a named context: logout, close its page (tab mode) or BrowserContext
|
||||
* (window mode), remove from registry. Cannot close the currently active
|
||||
* context — caller must setActiveContext to another first. This keeps the
|
||||
* recorder/page invariants simple: recorder is always attached to the
|
||||
* active slot, which closeContext never touches.
|
||||
*
|
||||
* @throws if name is not registered or equals the active context.
|
||||
*/
|
||||
export async function closeContext(name) {
|
||||
if (!contexts.has(name)) {
|
||||
throw new Error(`Context "${name}" not found. Available: [${[...contexts.keys()].join(', ')}]`);
|
||||
}
|
||||
if (name === activeContextName) {
|
||||
throw new Error(`closeContext: cannot close the active context "${name}". setActiveContext to another context first.`);
|
||||
}
|
||||
const slot = contexts.get(name);
|
||||
await _logoutSlot(slot);
|
||||
if (activeMode === 'tab') {
|
||||
try { await slot.page.close(); } catch {}
|
||||
} else {
|
||||
try { await slot.context.close(); } catch {}
|
||||
}
|
||||
contexts.delete(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Close startup modals and guide tabs.
|
||||
* Strategy: Escape → click default buttons → close extra tabs → repeat.
|
||||
@@ -4861,10 +5088,7 @@ export async function startRecording(outputPath, opts = {}) {
|
||||
const resolvedPath = resolveProjectPath(outputPath);
|
||||
mkdirSync(dirname(resolvedPath), { recursive: true });
|
||||
|
||||
// Create CDP session for screencast
|
||||
const cdp = await page.context().newCDPSession(page);
|
||||
|
||||
// Spawn ffmpeg process
|
||||
// Spawn ffmpeg process — single output file across context switches
|
||||
const ffmpeg = spawn(ffmpegPath, [
|
||||
'-y', // overwrite output
|
||||
'-f', 'image2pipe', // input: piped images
|
||||
@@ -4880,71 +5104,86 @@ export async function startRecording(outputPath, opts = {}) {
|
||||
resolvedPath
|
||||
], { stdio: ['pipe', 'ignore', 'pipe'] });
|
||||
|
||||
let ffmpegError = '';
|
||||
ffmpeg.stderr.on('data', d => { ffmpegError += d.toString(); });
|
||||
ffmpeg.on('error', err => { ffmpegError += err.message; });
|
||||
ffmpeg.on('error', err => { if (recorder) recorder.ffmpegError += err.message; });
|
||||
|
||||
// Listen for screencast frames and pipe to ffmpeg
|
||||
// CDP sends frames only on screen changes, so we duplicate frames
|
||||
// to fill gaps and maintain real-time playback speed
|
||||
const frameDuration = 1000 / fps;
|
||||
let lastFrameTime = null;
|
||||
let lastFrameBuf = null;
|
||||
const speechRate = opts.speechRate || 70; // ms per character for smart TTS wait
|
||||
|
||||
cdp.on('Page.screencastFrame', async ({ data, sessionId }) => {
|
||||
// Frame handler shared across CDP sessions (lives in recorder, not closure):
|
||||
// when the active context switches, we attach a new CDP session and route its
|
||||
// frames to the same ffmpeg pipe — preserving a single continuous timeline.
|
||||
const frameHandler = async ({ data, sessionId }, cdp) => {
|
||||
if (!recorder) return;
|
||||
const buf = Buffer.from(data, 'base64');
|
||||
const now = Date.now();
|
||||
|
||||
if (!ffmpeg.stdin.destroyed) {
|
||||
let framesWritten = 0;
|
||||
if (lastFrameTime && lastFrameBuf) {
|
||||
// Fill the gap with duplicates of the previous frame
|
||||
const gap = now - lastFrameTime;
|
||||
if (recorder.lastFrameTime && recorder.lastFrameBuf) {
|
||||
const gap = now - recorder.lastFrameTime;
|
||||
const dupes = Math.round(gap / frameDuration) - 1;
|
||||
for (let i = 0; i < dupes && i < fps * 30; i++) {
|
||||
ffmpeg.stdin.write(lastFrameBuf);
|
||||
ffmpeg.stdin.write(recorder.lastFrameBuf);
|
||||
framesWritten++;
|
||||
}
|
||||
}
|
||||
ffmpeg.stdin.write(buf);
|
||||
framesWritten++;
|
||||
// Track actual video timeline position (accounts for frame duplication)
|
||||
if (recorder) recorder.videoTimeMs += framesWritten * frameDuration;
|
||||
recorder.videoTimeMs += framesWritten * frameDuration;
|
||||
}
|
||||
|
||||
lastFrameTime = now;
|
||||
lastFrameBuf = buf;
|
||||
recorder.lastFrameTime = now;
|
||||
recorder.lastFrameBuf = buf;
|
||||
try { await cdp.send('Page.screencastFrameAck', { sessionId }); } catch {}
|
||||
});
|
||||
|
||||
// Start the screencast
|
||||
await cdp.send('Page.startScreencast', {
|
||||
format: 'jpeg',
|
||||
quality,
|
||||
everyNthFrame: 1
|
||||
});
|
||||
|
||||
// Expose a frame-writing helper on the recorder object.
|
||||
// During static periods (e.g. smart TTS pauses), CDP may not send screencast
|
||||
// frames. Call _flushFrames() to fill the gap with duplicates of the last frame,
|
||||
// keeping video timeline in sync with wall-clock time.
|
||||
const _flushFrames = () => {
|
||||
if (!lastFrameBuf || !lastFrameTime || ffmpeg.stdin.destroyed) return;
|
||||
const now = Date.now();
|
||||
const gap = now - lastFrameTime;
|
||||
const dupes = Math.round(gap / frameDuration);
|
||||
for (let i = 0; i < dupes; i++) {
|
||||
ffmpeg.stdin.write(lastFrameBuf);
|
||||
if (recorder) recorder.videoTimeMs += frameDuration;
|
||||
}
|
||||
if (dupes > 0) lastFrameTime = now;
|
||||
};
|
||||
|
||||
const speechRate = opts.speechRate || 70; // ms per character for smart TTS wait
|
||||
recorder = { cdp, ffmpeg, startTime: Date.now(), outputPath: resolvedPath, ffmpegError: '', captions: [], videoTimeMs: 0, _flushFrames, speechRate };
|
||||
// Redirect stderr accumulation to the recorder object
|
||||
ffmpeg.stderr.removeAllListeners('data');
|
||||
// Duplicate the last frame to fill wall-clock gaps (static periods, context switches).
|
||||
const _flushFrames = () => {
|
||||
if (!recorder || !recorder.lastFrameBuf || !recorder.lastFrameTime || ffmpeg.stdin.destroyed) return;
|
||||
const now = Date.now();
|
||||
const gap = now - recorder.lastFrameTime;
|
||||
const dupes = Math.round(gap / frameDuration);
|
||||
for (let i = 0; i < dupes; i++) {
|
||||
ffmpeg.stdin.write(recorder.lastFrameBuf);
|
||||
recorder.videoTimeMs += frameDuration;
|
||||
}
|
||||
if (dupes > 0) recorder.lastFrameTime = now;
|
||||
};
|
||||
|
||||
// Attach screencast to a specific page. Stops the old CDP first (if any).
|
||||
// Called by startRecording for the initial page, and by setActiveContext when
|
||||
// the active context changes mid-recording.
|
||||
const _attachPage = async (targetPage) => {
|
||||
if (recorder.cdp) {
|
||||
_flushFrames(); // freeze the last frame of the outgoing page up to "now"
|
||||
try { await recorder.cdp.send('Page.stopScreencast'); } catch {}
|
||||
try { await recorder.cdp.detach(); } catch {}
|
||||
recorder.cdp = null;
|
||||
}
|
||||
const cdp = await targetPage.context().newCDPSession(targetPage);
|
||||
cdp.on('Page.screencastFrame', (ev) => frameHandler(ev, cdp));
|
||||
await cdp.send('Page.startScreencast', { format: 'jpeg', quality, everyNthFrame: 1 });
|
||||
recorder.cdp = cdp;
|
||||
recorder.activePage = targetPage;
|
||||
};
|
||||
|
||||
recorder = {
|
||||
cdp: null,
|
||||
activePage: null,
|
||||
ffmpeg,
|
||||
startTime: Date.now(),
|
||||
outputPath: resolvedPath,
|
||||
ffmpegError: '',
|
||||
captions: [],
|
||||
videoTimeMs: 0,
|
||||
frameDuration,
|
||||
lastFrameTime: null,
|
||||
lastFrameBuf: null,
|
||||
_flushFrames,
|
||||
_attachPage,
|
||||
speechRate,
|
||||
};
|
||||
ffmpeg.stderr.on('data', d => { recorder.ffmpegError += d.toString(); });
|
||||
|
||||
await _attachPage(page);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -35,7 +35,7 @@ allowed-tools:
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File .claude/skills/web-unpublish/scripts/web-unpublish.ps1 <параметры>
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/web-unpublish.ps1" <параметры>
|
||||
```
|
||||
|
||||
### Параметры скрипта
|
||||
@@ -52,11 +52,11 @@ powershell.exe -NoProfile -File .claude/skills/web-unpublish/scripts/web-unpubli
|
||||
|
||||
```powershell
|
||||
# Удалить одну публикацию
|
||||
powershell.exe -NoProfile -File .claude/skills/web-unpublish/scripts/web-unpublish.ps1 -AppName "bpdemo"
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/web-unpublish.ps1" -AppName "bpdemo"
|
||||
|
||||
# Удалить все публикации
|
||||
powershell.exe -NoProfile -File .claude/skills/web-unpublish/scripts/web-unpublish.ps1 -All
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/web-unpublish.ps1" -All
|
||||
|
||||
# С указанием пути
|
||||
powershell.exe -NoProfile -File .claude/skills/web-unpublish/scripts/web-unpublish.ps1 -AppName "mydb" -ApachePath "C:\tools\apache24"
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/web-unpublish.ps1" -AppName "mydb" -ApachePath "C:\tools\apache24"
|
||||
```
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
# 1C Skills for {{PLATFORM_LABEL}} ({{RUNTIME_LABEL}})
|
||||
|
||||
Автоматическая сборка из [main]({{MAIN_REPO_URL}}) — навыки 1С:Предприятие 8.3 для AI-агента **{{PLATFORM_LABEL}}** с рантаймом **{{RUNTIME_LABEL}}**.
|
||||
|
||||
> Эта ветка генерируется CI на каждый push в main. **Не редактируйте напрямую** — все правки идут в [main]({{MAIN_REPO_URL}}).
|
||||
|
||||
## Установка
|
||||
|
||||
1. Скачайте ZIP этой ветки: **Code → Download ZIP** (или `git archive`).
|
||||
2. Распакуйте в корень своего проекта — должна появиться папка `{{PLATFORM_DIR}}/`.
|
||||
3. Запустите {{PLATFORM_LABEL}} из этого проекта — навыки станут доступны.
|
||||
|
||||
## Требования
|
||||
|
||||
- **Windows** с PowerShell 5.1+ (входит в Windows) — для PowerShell-сборки.
|
||||
- **Python 3.10+** — для Python-сборки. Зависимости: `lxml>=4.9.0`, `psutil>=5.9.0` (для DOM- и web-навыков).
|
||||
- **1С:Предприятие 8.3** — для сборки/разборки EPF/ERF и работы с базами.
|
||||
- **Node.js 18+** — для `/web-test`.
|
||||
|
||||
## Документация
|
||||
|
||||
Полные гайды, спецификации и описание навыков — в [main]({{MAIN_REPO_URL}}).
|
||||
|
||||
---
|
||||
|
||||
Source: {{MAIN_REPO_URL}}
|
||||
Build commit: `{{COMMIT_SHA}}`
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
|
||||
"name": "{{PLUGIN_NAME}}",
|
||||
"description": "[Python] Навыки для разработки на 1С:Предприятие 8.3 — абстракции над XML-форматами и CLI конфигуратора, плюс глаза и руки для тестирования через веб-клиент. Linux/Mac или когда PowerShell недоступен.",
|
||||
"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/"
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "{{PLUGIN_NAME}}",
|
||||
"version": "{{VERSION}}",
|
||||
"description": "[{{RUNTIME_LABEL}}] Навыки для разработки на 1С:Предприятие 8.3 — абстракции над XML-форматами и CLI конфигуратора, плюс глаза и руки для тестирования через веб-клиент.",
|
||||
"author": {
|
||||
"name": "Nikolay Shirokov"
|
||||
},
|
||||
"homepage": "https://github.com/Nikolay-Shirokov/cc-1c-skills",
|
||||
"repository": "https://github.com/Nikolay-Shirokov/cc-1c-skills",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"1c",
|
||||
"1c-dev",
|
||||
"cf",
|
||||
"cfe",
|
||||
"epf",
|
||||
"erf",
|
||||
"metadata",
|
||||
"configuration",
|
||||
"extension",
|
||||
"form",
|
||||
"report",
|
||||
"skd",
|
||||
"data-processor",
|
||||
"mxl",
|
||||
"web-client",
|
||||
"testing",
|
||||
"test-automation"
|
||||
],
|
||||
"skills": "./.codex/skills/",
|
||||
"interface": {
|
||||
"displayName": "1C Skills ({{RUNTIME_LABEL}})",
|
||||
"shortDescription": "{{SHORT_DESCRIPTION}}",
|
||||
"category": "Development"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
name: Build port branches
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- '.claude/skills/**'
|
||||
- 'scripts/switch.py'
|
||||
- '.github/templates/README.port.md.tmpl'
|
||||
- '.github/templates/codex-plugin.json.tmpl'
|
||||
- '.github/templates/claude-plugin.json.tmpl'
|
||||
- '.github/workflows/build-ports.yml'
|
||||
- 'LICENSE'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- platform: claude-code
|
||||
runtime: python
|
||||
branch: port-claude-code-py
|
||||
label: Claude Code
|
||||
target_dir: .claude/skills
|
||||
- platform: cursor
|
||||
runtime: powershell
|
||||
branch: port-cursor
|
||||
label: Cursor
|
||||
target_dir: .cursor/skills
|
||||
- platform: cursor
|
||||
runtime: python
|
||||
branch: port-cursor-py
|
||||
label: Cursor
|
||||
target_dir: .cursor/skills
|
||||
- platform: codex
|
||||
runtime: powershell
|
||||
branch: port-codex
|
||||
label: Codex
|
||||
target_dir: .codex/skills
|
||||
- platform: codex
|
||||
runtime: python
|
||||
branch: port-codex-py
|
||||
label: Codex
|
||||
target_dir: .codex/skills
|
||||
- platform: copilot
|
||||
runtime: powershell
|
||||
branch: port-copilot
|
||||
label: GitHub Copilot
|
||||
target_dir: .github/skills
|
||||
- platform: copilot
|
||||
runtime: python
|
||||
branch: port-copilot-py
|
||||
label: GitHub Copilot
|
||||
target_dir: .github/skills
|
||||
- platform: augment
|
||||
runtime: powershell
|
||||
branch: port-augment
|
||||
label: Augment
|
||||
target_dir: .augment/skills
|
||||
- platform: augment
|
||||
runtime: python
|
||||
branch: port-augment-py
|
||||
label: Augment
|
||||
target_dir: .augment/skills
|
||||
- platform: cline
|
||||
runtime: powershell
|
||||
branch: port-cline
|
||||
label: Cline
|
||||
target_dir: .cline/skills
|
||||
- platform: cline
|
||||
runtime: python
|
||||
branch: port-cline-py
|
||||
label: Cline
|
||||
target_dir: .cline/skills
|
||||
- platform: kilo
|
||||
runtime: powershell
|
||||
branch: port-kilo
|
||||
label: Kilo Code
|
||||
target_dir: .kilocode/skills
|
||||
- platform: kilo
|
||||
runtime: python
|
||||
branch: port-kilo-py
|
||||
label: Kilo Code
|
||||
target_dir: .kilocode/skills
|
||||
- platform: kiro
|
||||
runtime: powershell
|
||||
branch: port-kiro
|
||||
label: Kiro
|
||||
target_dir: .kiro/skills
|
||||
- platform: kiro
|
||||
runtime: python
|
||||
branch: port-kiro-py
|
||||
label: Kiro
|
||||
target_dir: .kiro/skills
|
||||
- platform: gemini
|
||||
runtime: powershell
|
||||
branch: port-gemini
|
||||
label: Gemini CLI
|
||||
target_dir: .gemini/skills
|
||||
- platform: gemini
|
||||
runtime: python
|
||||
branch: port-gemini-py
|
||||
label: Gemini CLI
|
||||
target_dir: .gemini/skills
|
||||
- platform: opencode
|
||||
runtime: powershell
|
||||
branch: port-opencode
|
||||
label: OpenCode
|
||||
target_dir: .opencode/skills
|
||||
- platform: opencode
|
||||
runtime: python
|
||||
branch: port-opencode-py
|
||||
label: OpenCode
|
||||
target_dir: .opencode/skills
|
||||
- platform: roo
|
||||
runtime: powershell
|
||||
branch: port-roo
|
||||
label: Roo Code
|
||||
target_dir: .roo/skills
|
||||
- platform: roo
|
||||
runtime: python
|
||||
branch: port-roo-py
|
||||
label: Roo Code
|
||||
target_dir: .roo/skills
|
||||
- platform: windsurf
|
||||
runtime: powershell
|
||||
branch: port-windsurf
|
||||
label: Windsurf
|
||||
target_dir: .windsurf/skills
|
||||
- platform: windsurf
|
||||
runtime: python
|
||||
branch: port-windsurf-py
|
||||
label: Windsurf
|
||||
target_dir: .windsurf/skills
|
||||
- platform: agents
|
||||
runtime: powershell
|
||||
branch: port-agents
|
||||
label: Agent Skills
|
||||
target_dir: .agents/skills
|
||||
- platform: agents
|
||||
runtime: python
|
||||
branch: port-agents-py
|
||||
label: Agent Skills
|
||||
target_dir: .agents/skills
|
||||
|
||||
steps:
|
||||
- name: Checkout main
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Build skills tree for ${{ matrix.platform }} (${{ matrix.runtime }})
|
||||
run: |
|
||||
python scripts/switch.py "${{ matrix.platform }}" \
|
||||
--project-dir build \
|
||||
--runtime "${{ matrix.runtime }}"
|
||||
|
||||
- name: Render port README
|
||||
env:
|
||||
PLATFORM_LABEL: ${{ matrix.label }}
|
||||
PLATFORM_DIR: ${{ matrix.target_dir }}
|
||||
RUNTIME_LABEL: ${{ matrix.runtime == 'powershell' && 'PowerShell' || 'Python' }}
|
||||
COMMIT_SHA: ${{ github.sha }}
|
||||
MAIN_REPO_URL: https://github.com/${{ github.repository }}
|
||||
run: |
|
||||
sed \
|
||||
-e "s|{{PLATFORM_LABEL}}|${PLATFORM_LABEL}|g" \
|
||||
-e "s|{{PLATFORM_DIR}}|${PLATFORM_DIR}|g" \
|
||||
-e "s|{{RUNTIME_LABEL}}|${RUNTIME_LABEL}|g" \
|
||||
-e "s|{{COMMIT_SHA}}|${COMMIT_SHA}|g" \
|
||||
-e "s|{{MAIN_REPO_URL}}|${MAIN_REPO_URL}|g" \
|
||||
.github/templates/README.port.md.tmpl > build/README.md
|
||||
|
||||
- name: Render Codex plugin manifest
|
||||
if: matrix.platform == 'codex'
|
||||
env:
|
||||
PLUGIN_NAME: ${{ matrix.runtime == 'python' && '1c-skills-py' || '1c-skills' }}
|
||||
RUNTIME_LABEL: ${{ matrix.runtime == 'powershell' && 'PowerShell' || 'Python' }}
|
||||
SHORT_DESCRIPTION: ${{ matrix.runtime == 'python' && 'Python runtime (Linux/Mac/Windows)' || 'PowerShell runtime (Windows-first)' }}
|
||||
COMMIT_SHA: ${{ github.sha }}
|
||||
run: |
|
||||
VERSION="$(date -u +%Y.%-m.%-d)+${COMMIT_SHA::7}"
|
||||
mkdir -p build/.codex-plugin
|
||||
sed \
|
||||
-e "s|{{PLUGIN_NAME}}|${PLUGIN_NAME}|g" \
|
||||
-e "s|{{VERSION}}|${VERSION}|g" \
|
||||
-e "s|{{RUNTIME_LABEL}}|${RUNTIME_LABEL}|g" \
|
||||
-e "s|{{SHORT_DESCRIPTION}}|${SHORT_DESCRIPTION}|g" \
|
||||
.github/templates/codex-plugin.json.tmpl > build/.codex-plugin/plugin.json
|
||||
|
||||
- name: Render Claude plugin manifest (Py variant)
|
||||
if: matrix.platform == 'claude-code' && matrix.runtime == 'python'
|
||||
env:
|
||||
PLUGIN_NAME: 1c-skills-py
|
||||
run: |
|
||||
mkdir -p build/.claude-plugin
|
||||
sed -e "s|{{PLUGIN_NAME}}|${PLUGIN_NAME}|g" \
|
||||
.github/templates/claude-plugin.json.tmpl > build/.claude-plugin/plugin.json
|
||||
|
||||
- name: Copy LICENSE
|
||||
run: cp LICENSE build/LICENSE
|
||||
|
||||
- name: Force-push orphan snapshot to ${{ matrix.branch }}
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
cd build
|
||||
git init -q -b master
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git add -A
|
||||
git commit -q -m "Auto-build: ${{ matrix.platform }} (${{ matrix.runtime }}) from ${GITHUB_SHA::7}"
|
||||
git push --force \
|
||||
"https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git" \
|
||||
"master:${{ matrix.branch }}"
|
||||
+1
-1
@@ -37,7 +37,7 @@ __pycache__/
|
||||
*.mp4
|
||||
|
||||
# Навыки, скопированные для других AI-платформ (генерируются scripts/switch.py)
|
||||
.agents/
|
||||
.agents/skills/
|
||||
.augment/
|
||||
.cline/
|
||||
.codex/
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
# 1C Skills for Claude Code
|
||||
|
||||
> **Work in progress** — навыки находятся в стадии тестирования, отладки и оптимизации.
|
||||
> **Проект живой, активно развивается** — добавляются новые возможности, отлавливаются и исправляются баги, производится оптимизация. Следите за обновлениями.
|
||||
|
||||
Набор навыков для AI-агентов (в первую очередь для [Claude Code](https://docs.anthropic.com/en/docs/claude-code/skills)), помогающий охватить полный цикл разработки на платформе 1С:Предприятие 8.3 — от создания конфигураций, расширений, внешних обработок и отчётов до загрузки изменений в информационную базу, обновления, запуска, публикации на веб-сервере (портативная версия Apache), тестирования через веб-клиент и записи видеоинструкций.
|
||||
|
||||
Навыки дают модели готовые абстракции над XML-форматами и CLI конфигуратора — чтобы работать с сутью задачи, а не с деталями реализации. А веб-тестирование даёт ей глаза и руки для взаимодействия с интерфейсом 1С.
|
||||
|
||||
## Версии навыков для разных платформ
|
||||
|
||||
> **PS** — версия со скриптами на PowerShell (стандартная, для Windows). **Py** — версия со скриптами на Python (если PowerShell не подходит). Можно начать с **PS** — это основной вариант.
|
||||
|
||||
**Claude Code** [PS](https://github.com/Nikolay-Shirokov/cc-1c-skills/tree/main) · [Py](https://github.com/Nikolay-Shirokov/cc-1c-skills/tree/port-claude-code-py) — **Cursor** [PS](https://github.com/Nikolay-Shirokov/cc-1c-skills/tree/port-cursor) · [Py](https://github.com/Nikolay-Shirokov/cc-1c-skills/tree/port-cursor-py) — **Codex** [PS](https://github.com/Nikolay-Shirokov/cc-1c-skills/tree/port-codex) · [Py](https://github.com/Nikolay-Shirokov/cc-1c-skills/tree/port-codex-py) — [Другие платформы →](#поддерживаемые-платформы)
|
||||
|
||||
## Быстрый старт
|
||||
|
||||
Скопируйте каталог `.claude/skills/` из этого репозитория в корень вашего проекта. Навыки станут доступны при запуске Claude Code из этого каталога.
|
||||
@@ -16,6 +22,21 @@
|
||||
└── ...
|
||||
```
|
||||
|
||||
Или установите как плагин Claude Code:
|
||||
|
||||
```
|
||||
/plugin marketplace add https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
/plugin install 1c-skills@cc-1c-skills # PowerShell (Windows)
|
||||
/plugin install 1c-skills-py@cc-1c-skills # Python (Linux/Mac или если PS недоступен)
|
||||
```
|
||||
|
||||
Или установите как плагин OpenAI Codex:
|
||||
|
||||
```
|
||||
codex plugin marketplace add Nikolay-Shirokov/cc-1c-skills
|
||||
codex /plugins # выберите 1c-skills (PowerShell) или 1c-skills-py (Python) и нажмите Install
|
||||
```
|
||||
|
||||
Или используйте скрипт установки:
|
||||
|
||||
```bash
|
||||
@@ -53,6 +74,7 @@ python tools/cc-1c-skills/scripts/switch.py
|
||||
| Веб-публикация (Web) | 4 навыка `/web-*` | Публикация баз через Apache, статус, остановка, удаление публикаций | [Подробнее](docs/web-guide.md) |
|
||||
| Тестирование (Web) | `/web-test` | Взаимодействие с веб-клиентом 1С — навигация, формы, таблицы, отчёты, тестирование | [Подробнее](docs/web-test-guide.md) |
|
||||
| Запись видео (Web) | `/web-test` | Запись видеоинструкций с субтитрами, подсветкой и TTS-озвучкой | [Подробнее](docs/web-test-recording-guide.md) |
|
||||
| Регресс прикладного решения (Web) | `/web-test` | Автоматический регресс конфигурации: тесты, проверки, отчёты, прогон после правок | [Подробнее](docs/web-test-regression-guide.md) |
|
||||
| Утилиты | `/img-grid` | Наложение сетки на изображение для определения пропорций колонок | — |
|
||||
|
||||
## Требования
|
||||
@@ -61,9 +83,37 @@ python tools/cc-1c-skills/scripts/switch.py
|
||||
- **1С:Предприятие 8.3** — для сборки/разборки EPF/ERF (навыки генерации XML работают без платформы)
|
||||
- **Node.js 18+** — для `/web-test` (тестирование через браузер)
|
||||
|
||||
<a id="поддерживаемые-платформы"></a>
|
||||
### Другие AI-платформы
|
||||
|
||||
Навыки построены на открытом стандарте [Agent Skills](https://agentskills.io/specification) и совместимы с любой платформой, поддерживающей этот формат. Скрипт `switch.py` копирует навыки в нужный каталог с перезаписью путей:
|
||||
Навыки построены на открытом стандарте [Agent Skills](https://agentskills.io/specification) и совместимы с любой платформой, поддерживающей этот формат. Самый быстрый путь — скачать готовую сборку под нужную платформу из ветки ниже (**Code → Download ZIP**) и распаковать в корень своего проекта.
|
||||
|
||||
#### Поддерживаемые платформы
|
||||
|
||||
| Платформа | Целевой каталог | PowerShell | Python |
|
||||
|-----------|----------------|------------|--------|
|
||||
| Claude Code | `.claude/skills/` | [main](https://github.com/Nikolay-Shirokov/cc-1c-skills/tree/main) | [port-claude-code-py](https://github.com/Nikolay-Shirokov/cc-1c-skills/tree/port-claude-code-py) |
|
||||
| Cursor | `.cursor/skills/` | [port-cursor](https://github.com/Nikolay-Shirokov/cc-1c-skills/tree/port-cursor) | [port-cursor-py](https://github.com/Nikolay-Shirokov/cc-1c-skills/tree/port-cursor-py) |
|
||||
| OpenAI Codex | `.codex/skills/` | [port-codex](https://github.com/Nikolay-Shirokov/cc-1c-skills/tree/port-codex) | [port-codex-py](https://github.com/Nikolay-Shirokov/cc-1c-skills/tree/port-codex-py) |
|
||||
| GitHub Copilot | `.github/skills/` | [port-copilot](https://github.com/Nikolay-Shirokov/cc-1c-skills/tree/port-copilot) | [port-copilot-py](https://github.com/Nikolay-Shirokov/cc-1c-skills/tree/port-copilot-py) |
|
||||
| Augment | `.augment/skills/` | [port-augment](https://github.com/Nikolay-Shirokov/cc-1c-skills/tree/port-augment) | [port-augment-py](https://github.com/Nikolay-Shirokov/cc-1c-skills/tree/port-augment-py) |
|
||||
| Cline | `.cline/skills/` | [port-cline](https://github.com/Nikolay-Shirokov/cc-1c-skills/tree/port-cline) | [port-cline-py](https://github.com/Nikolay-Shirokov/cc-1c-skills/tree/port-cline-py) |
|
||||
| Kilo Code | `.kilocode/skills/` | [port-kilo](https://github.com/Nikolay-Shirokov/cc-1c-skills/tree/port-kilo) | [port-kilo-py](https://github.com/Nikolay-Shirokov/cc-1c-skills/tree/port-kilo-py) |
|
||||
| Kiro | `.kiro/skills/` | [port-kiro](https://github.com/Nikolay-Shirokov/cc-1c-skills/tree/port-kiro) | [port-kiro-py](https://github.com/Nikolay-Shirokov/cc-1c-skills/tree/port-kiro-py) |
|
||||
| Gemini CLI | `.gemini/skills/` | [port-gemini](https://github.com/Nikolay-Shirokov/cc-1c-skills/tree/port-gemini) | [port-gemini-py](https://github.com/Nikolay-Shirokov/cc-1c-skills/tree/port-gemini-py) |
|
||||
| OpenCode | `.opencode/skills/` | [port-opencode](https://github.com/Nikolay-Shirokov/cc-1c-skills/tree/port-opencode) | [port-opencode-py](https://github.com/Nikolay-Shirokov/cc-1c-skills/tree/port-opencode-py) |
|
||||
| Roo Code | `.roo/skills/` | [port-roo](https://github.com/Nikolay-Shirokov/cc-1c-skills/tree/port-roo) | [port-roo-py](https://github.com/Nikolay-Shirokov/cc-1c-skills/tree/port-roo-py) |
|
||||
| Windsurf | `.windsurf/skills/` | [port-windsurf](https://github.com/Nikolay-Shirokov/cc-1c-skills/tree/port-windsurf) | [port-windsurf-py](https://github.com/Nikolay-Shirokov/cc-1c-skills/tree/port-windsurf-py) |
|
||||
| Agent Skills | `.agents/skills/` | [port-agents](https://github.com/Nikolay-Shirokov/cc-1c-skills/tree/port-agents) | [port-agents-py](https://github.com/Nikolay-Shirokov/cc-1c-skills/tree/port-agents-py) |
|
||||
|
||||
Готовые ветки `port-*` пересобираются автоматически на каждое изменение в навыках. Если нужна свежая сборка прямо сейчас — соберите [локально через `switch.py`](#альтернативный-способ--собрать-локально-через-switchpy).
|
||||
|
||||
Некоторые платформы (Augment, Cline, VS Code/Copilot) также сканируют `.claude/skills/` как fallback — для них достаточно `main`.
|
||||
|
||||
<a id="альтернативный-способ--собрать-локально-через-switchpy"></a>
|
||||
#### Альтернативный способ — собрать локально через `switch.py`
|
||||
|
||||
Если нужна свежая сборка под свою платформу (или платформа ещё не в матрице port-веток):
|
||||
|
||||
```bash
|
||||
python scripts/switch.py # интерактивный режим
|
||||
@@ -80,26 +130,6 @@ python scripts/switch.py --undo cursor # удалить
|
||||
|
||||
> ⚠ **Известные ограничения `--link`:** Node.js резолвит `__dirname` через junction к реальному пути источника, а не к каталогу проекта. Это может приводить к тому, что навыки с Node.js-скриптами (например, `/web-test`) будут записывать файлы в каталог репозитория навыков вместо каталога проекта. При возникновении проблем переключитесь на копирование (без `--link`).
|
||||
|
||||
Поддерживаемые платформы:
|
||||
|
||||
| Платформа | Целевой каталог | `switch.py <platform>` |
|
||||
|-----------|----------------|------------------------|
|
||||
| Claude Code | `.claude/skills/` | `claude-code` |
|
||||
| Augment | `.augment/skills/` | `augment` |
|
||||
| Cline | `.cline/skills/` | `cline` |
|
||||
| Cursor | `.cursor/skills/` | `cursor` |
|
||||
| GitHub Copilot | `.github/skills/` | `copilot` |
|
||||
| Kilo Code | `.kilocode/skills/` | `kilo` |
|
||||
| Kiro | `.kiro/skills/` | `kiro` |
|
||||
| OpenAI Codex | `.codex/skills/` | `codex` |
|
||||
| Gemini CLI | `.gemini/skills/` | `gemini` |
|
||||
| OpenCode | `.opencode/skills/` | `opencode` |
|
||||
| Roo Code | `.roo/skills/` | `roo` |
|
||||
| Windsurf | `.windsurf/skills/` | `windsurf` |
|
||||
| Agent Skills | `.agents/skills/` | `agents` |
|
||||
|
||||
Некоторые платформы (Augment, Cline, VS Code/Copilot) также сканируют `.claude/skills/` как fallback — для них копирование необязательно, но `switch.py` даёт явный контроль над путями.
|
||||
|
||||
Автоактивация — основной режим: просто опишите задачу своими словами, ассистент сам подберёт нужный навык по `description` в SKILL.md. Слеш-команды (например `/epf-init`) — для точного контроля, когда нужно вызвать конкретный навык.
|
||||
|
||||
### Переключение рантайма (PowerShell ↔ Python)
|
||||
@@ -226,6 +256,7 @@ docs/
|
||||
├── web-guide.md # Гайд: веб-публикация через Apache
|
||||
├── web-test-guide.md # Гайд: тестирование через веб-клиент
|
||||
├── web-test-recording-guide.md # Гайд: запись видеоинструкций
|
||||
├── web-test-regression-guide.md # Гайд: регресс прикладного решения
|
||||
├── 1c-epf-spec.md # Спецификация XML-формата (EPF)
|
||||
├── 1c-erf-spec.md # Спецификация XML-формата (ERF)
|
||||
├── 1c-form-spec.md # Спецификация управляемых форм
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user