mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-07-11 05:23:42 +03:00
Compare commits
203 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3d36c20269 | |||
| e507e6bfba | |||
| 293e8e7a55 | |||
| 6787e97a72 | |||
| 5ad2e4b5fe | |||
| b9c7af02de | |||
| 58ea52cb63 | |||
| 6e458bf0b8 | |||
| 63fd91c3ca | |||
| a991458ef2 | |||
| 4ec2420af6 | |||
| ba0880a5c5 | |||
| 378b19b59f | |||
| ebd620d262 | |||
| 07ea676326 | |||
| de04a8dc7a | |||
| acbd6be46c | |||
| b17dd5d04c | |||
| 2136245b69 | |||
| 489b8389aa | |||
| 96660f4d9d | |||
| 75a97d51ea | |||
| 8817f37bf0 | |||
| ae82412377 | |||
| a38bb55bca | |||
| 1c21bef26c | |||
| 09d7097476 | |||
| 717f3d8cc5 | |||
| e8b8d32e0d | |||
| 2a8d594f66 | |||
| 06331a9b80 | |||
| 9ec5857e22 | |||
| b5e8e1df7a | |||
| 95d8ece309 | |||
| 26c804391a | |||
| 4855f79403 | |||
| 4434493446 | |||
| 47e980f932 | |||
| 6cfc504509 | |||
| 831c80d9f0 | |||
| db341f2351 | |||
| 2abaa28f16 | |||
| 03720d93ed | |||
| 90d2649a5f | |||
| e1fb40189c | |||
| 2d326c99a5 | |||
| 8465bbc82e | |||
| 8eedca4c22 | |||
| 670a574249 | |||
| e3ae9c27d1 | |||
| 85ae72739f | |||
| 668173121d | |||
| 9af86b7810 | |||
| dc56ef6899 | |||
| a41a6d822b | |||
| 8631026259 | |||
| e127dfcf3d | |||
| ed2339a4bc | |||
| 71ecec6594 | |||
| dd32d2a6ca | |||
| 227423ee1f | |||
| c0487c51b7 | |||
| 934462f4d2 | |||
| 092f30a663 | |||
| 03b2b5a64e | |||
| abcd5be2b0 | |||
| aa39973ae6 | |||
| dab122c166 | |||
| 73b07a6fbc | |||
| 43d36119cb | |||
| 081d3a8a2f | |||
| c383cc4ffe | |||
| 01e5de8acf | |||
| 8542b45719 | |||
| 819b7fa126 | |||
| b794560492 | |||
| 3340d48898 | |||
| cb3dda0d53 | |||
| e007233c91 | |||
| 569234b448 | |||
| d0361561ca | |||
| 75b77caf9b | |||
| da1d8c9297 | |||
| 6377cdfba5 | |||
| 6e4fdb443a | |||
| 330447c95f | |||
| bdd38691aa | |||
| 2ab1b5bc14 | |||
| 9900f8f656 | |||
| 32273d2be8 | |||
| c96bcc3566 | |||
| 89bb58a101 | |||
| 3ce71d436f | |||
| 459d3feb69 | |||
| b781f97832 | |||
| b2eaa9e129 | |||
| c4f600a36b | |||
| 606ac10fdb | |||
| 73f1e6ec26 | |||
| a3395d4abe | |||
| dd9af25e67 | |||
| 6d9c41507c | |||
| d8689b3674 | |||
| d5d19710cb | |||
| dd226697bf | |||
| 1333e09ff8 | |||
| f534add7b4 | |||
| f064d53eb2 | |||
| 29f288fe1d | |||
| 5ebc02d0b3 | |||
| ca96a7413a | |||
| 2ff99d1b77 | |||
| e16b23968e | |||
| d7dedd4843 | |||
| ef036c7cf1 | |||
| b5769ce373 | |||
| 7905615091 | |||
| 24e7116b7d | |||
| 1ed23b2a08 | |||
| cc8b283f1b | |||
| bc81faf892 | |||
| 1a601a4d0c | |||
| 8aad132cc3 | |||
| fd9539dd20 | |||
| 6807b07681 | |||
| 8bff8d7e05 | |||
| 4916f5bf7c | |||
| 7882c1cc2b | |||
| 4a68a977dc | |||
| ab9b4c6197 | |||
| f7d5e2fd00 | |||
| 8915e99ac8 | |||
| ea43522b5a | |||
| fb3bce5811 | |||
| 8ac0dfefd0 | |||
| 9f672044ff | |||
| 7a2c257721 | |||
| 5a67c56e92 | |||
| 09c3dcd988 | |||
| d06c2c49bb | |||
| 01b598ce8f | |||
| 41e4714773 | |||
| ad89929efd | |||
| 929d1676bb | |||
| d0bb26b068 | |||
| f9ae24a678 | |||
| 339c70b457 | |||
| 65f63fbc48 | |||
| 0397d8f37e | |||
| d88859580f | |||
| d5d525aa27 | |||
| 1888952a41 | |||
| ff93d169a8 | |||
| b692a81ea5 | |||
| bc53ee2a14 | |||
| 0636ac6877 | |||
| cfce486004 | |||
| 2367eaa353 | |||
| 8448a28a29 | |||
| 4430ebc42e | |||
| 5112dbec9e | |||
| 701d56b075 | |||
| 27f5da4829 | |||
| 786bdf97d9 | |||
| 6056a4a5af | |||
| fdbfa3b643 | |||
| 9b77f06aba | |||
| daf7f1526a | |||
| 7eb825b3a7 | |||
| c998139c89 | |||
| 7b945e786d | |||
| a1a22d1ffe | |||
| 7c765137db | |||
| d484a5b7ec | |||
| f34993b805 | |||
| b147e491ee | |||
| 684cd17d5f | |||
| e905d5f576 | |||
| 1b56e7a735 | |||
| a9e1ab64c8 | |||
| 1d158e3218 | |||
| 15883a7e7c | |||
| 6857ad5060 | |||
| 908af27bf0 | |||
| 22e929ecb3 | |||
| c43041c0b7 | |||
| 8998c0b5db | |||
| 4c2c72abce | |||
| a38874280c | |||
| bccdf93094 | |||
| d935cfe0cc | |||
| 3483802ab0 | |||
| 3b0061c8a0 | |||
| b4fc9bf42c | |||
| f27a17139a | |||
| 9c1ea1662a | |||
| 0941fc717d | |||
| 4ba1e595bf | |||
| e777ded8d2 | |||
| 67eaa1c3c8 | |||
| 7c38422e2c | |||
| 6d119eb473 | |||
| 9877fe403a |
@@ -1,4 +1,4 @@
|
||||
# cf-edit v1.4 — Edit 1C configuration root (Configuration.xml)
|
||||
# cf-edit v1.7 — Edit 1C configuration root (Configuration.xml)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory)][Alias('Path')][string]$ConfigPath,
|
||||
@@ -29,6 +29,126 @@ if (-not (Test-Path $ConfigPath)) { Write-Error "File not found: $ConfigPath"; e
|
||||
$resolvedPath = (Resolve-Path $ConfigPath).Path
|
||||
$script:configDir = [System.IO.Path]::GetDirectoryName($resolvedPath)
|
||||
|
||||
# --- Support guard (Ext/ParentConfigurations.bin) ---
|
||||
# See docs/1c-support-state-spec.md. Blocks edits of vendor objects "на замке" /
|
||||
# read-only configs unless allowed. Trigger = bin present; reaction from
|
||||
# .v8-project.json editingAllowedCheck (deny|warn|off, default deny). Never
|
||||
# throws — guard errors degrade to allow.
|
||||
function Get-RootUuid([string]$xmlPath) {
|
||||
if (-not (Test-Path $xmlPath)) { return $null }
|
||||
try {
|
||||
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
|
||||
$el = $mx.DocumentElement.FirstChild
|
||||
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
|
||||
if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } }
|
||||
} catch {}
|
||||
return $null
|
||||
}
|
||||
function Find-V8Project([string]$startDir) {
|
||||
$d = $startDir
|
||||
for ($i = 0; $i -lt 20 -and $d; $i++) {
|
||||
$pj = Join-Path $d ".v8-project.json"
|
||||
if (Test-Path $pj) { return $pj }
|
||||
$parent = [System.IO.Path]::GetDirectoryName($d)
|
||||
if ($parent -eq $d) { break }
|
||||
$d = $parent
|
||||
}
|
||||
return $null
|
||||
}
|
||||
function Get-EditMode([string]$cfgDir) {
|
||||
try {
|
||||
$pj = Find-V8Project (Get-Location).Path
|
||||
if (-not $pj) { $pj = Find-V8Project $cfgDir }
|
||||
if (-not $pj) { return 'deny' }
|
||||
$proj = Get-Content -Raw $pj | ConvertFrom-Json
|
||||
$cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/')
|
||||
if ($proj.databases) {
|
||||
foreach ($db in $proj.databases) {
|
||||
if ($db.configSrc) {
|
||||
$src = [System.IO.Path]::GetFullPath($db.configSrc).TrimEnd('\', '/')
|
||||
if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) {
|
||||
if ($db.editingAllowedCheck) { return $db.editingAllowedCheck }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($proj.editingAllowedCheck) { return $proj.editingAllowedCheck }
|
||||
return 'deny'
|
||||
} catch { return 'deny' }
|
||||
}
|
||||
function Assert-EditAllowed([string]$targetPath, [string]$require) {
|
||||
try {
|
||||
$rp = $targetPath
|
||||
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
|
||||
$elemUuid = Get-RootUuid $rp
|
||||
$cfgDir = $null; $binPath = $null
|
||||
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
|
||||
for ($i = 0; $i -lt 12 -and $d; $i++) {
|
||||
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
|
||||
if (-not $cfgDir) {
|
||||
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
|
||||
if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $cfgDir = $d; $binPath = $cand }
|
||||
}
|
||||
if ($elemUuid -and $cfgDir) { break }
|
||||
$parent = [System.IO.Path]::GetDirectoryName($d)
|
||||
if ($parent -eq $d) { break }
|
||||
$d = $parent
|
||||
}
|
||||
# New object (no element file): fall back to config root uuid.
|
||||
if (-not $elemUuid -and $cfgDir) { $elemUuid = Get-RootUuid (Join-Path $cfgDir "Configuration.xml") }
|
||||
if (-not $binPath -or -not (Test-Path $binPath)) { return }
|
||||
$bytes = [System.IO.File]::ReadAllBytes($binPath)
|
||||
if ($bytes.Length -le 32) { return }
|
||||
$start = 0
|
||||
if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 }
|
||||
$text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start)
|
||||
$hm = [regex]::Match($text, '^\{6,(\d+),(\d+),')
|
||||
if (-not $hm.Success) { return }
|
||||
$G = [int]$hm.Groups[1].Value
|
||||
$K = [int]$hm.Groups[2].Value
|
||||
if ($K -eq 0) { return }
|
||||
$best = $null
|
||||
if ($elemUuid) {
|
||||
$u = [regex]::Escape($elemUuid.ToLower())
|
||||
foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) {
|
||||
$f1 = [int]$m.Groups[1].Value
|
||||
if ($null -eq $best -or $f1 -lt $best) { $best = $f1 }
|
||||
}
|
||||
}
|
||||
$blocked = $false; $code = ""; $reason = ""
|
||||
if ($G -eq 1) { $blocked = $true; $code = "capability-off"; $reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)" }
|
||||
elseif ($require -eq 'removed') {
|
||||
if ($null -ne $best -and $best -ne 2) { $blocked = $true; $code = "not-removed"; $reason = "объект не снят с поддержки — удаление сломает обновления" }
|
||||
}
|
||||
else {
|
||||
if ($null -ne $best -and $best -eq 0) { $blocked = $true; $code = "locked"; $reason = "объект на замке — редактирование сломает обновления" }
|
||||
}
|
||||
if (-not $blocked) { return }
|
||||
$mode = Get-EditMode $cfgDir
|
||||
if ($mode -eq 'off') { return }
|
||||
# Use Console.Error (not Write-Error) — under ErrorActionPreference=Stop the
|
||||
# latter throws and would be swallowed by this function's own catch.
|
||||
if ($mode -eq 'warn') { [Console]::Error.WriteLine("[support-guard] ПРЕДУПРЕЖДЕНИЕ: $reason. Цель: $rp"); return }
|
||||
$head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления."
|
||||
$cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются."
|
||||
$offNote = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json."
|
||||
if ($code -eq "capability-off") {
|
||||
$state = "Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «$rp» редактировать нельзя."
|
||||
$fix = "Либо снять защиту явно (навык support-edit, два шага):`n 1. support-edit -Path ""$cfgDir"" -Capability on — включить возможность изменения (объекты пока остаются на замке);`n 2. support-edit -Path ""$rp"" -Set editable — открыть этот объект для редактирования.`n Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора."
|
||||
} elseif ($code -eq "not-removed") {
|
||||
$state = "Состояние: объект «$rp» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора."
|
||||
$fix = "Либо сначала снять объект с поддержки, затем удалять:`n support-edit -Path ""$rp"" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно."
|
||||
} else {
|
||||
$state = "Состояние: объект «$rp» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)."
|
||||
$fix = "Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):`n support-edit -Path ""$rp"" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);`n support-edit -Path ""$rp"" -Set off-support — снять с поддержки: обновления по объекту больше не приходят."
|
||||
}
|
||||
[Console]::Error.WriteLine("$head`n$state`n$cfe`n$fix`n$offNote")
|
||||
exit 1
|
||||
} catch { return }
|
||||
}
|
||||
|
||||
Assert-EditAllowed $resolvedPath 'editable'
|
||||
|
||||
# --- Load XML with PreserveWhitespace ---
|
||||
$script:xmlDoc = New-Object System.Xml.XmlDocument
|
||||
$script:xmlDoc.PreserveWhitespace = $true
|
||||
|
||||
@@ -1,16 +1,177 @@
|
||||
#!/usr/bin/env python3
|
||||
# cf-edit v1.4 — Edit 1C configuration root (Configuration.xml)
|
||||
# cf-edit v1.7 — Edit 1C configuration root (Configuration.xml)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import uuid as _uuid
|
||||
from html import escape as html_escape
|
||||
from lxml import etree
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md
|
||||
# Blocks edits of vendor objects "на замке" / read-only configs. Trigger = bin
|
||||
# present; reaction from .v8-project.json editingAllowedCheck (deny|warn|off,
|
||||
# default deny). Never throws (except sys.exit on deny) — errors degrade to allow.
|
||||
# ============================================================
|
||||
|
||||
def _sg_root_uuid(xml_path):
|
||||
if not os.path.isfile(xml_path):
|
||||
return None
|
||||
try:
|
||||
mx = etree.parse(xml_path).getroot()
|
||||
for child in mx:
|
||||
if isinstance(child.tag, str) and child.get("uuid"):
|
||||
return child.get("uuid")
|
||||
except Exception:
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def _sg_find_v8project(start_dir):
|
||||
d = start_dir
|
||||
for _ in range(20):
|
||||
if not d:
|
||||
break
|
||||
pj = os.path.join(d, ".v8-project.json")
|
||||
if os.path.isfile(pj):
|
||||
return pj
|
||||
parent = os.path.dirname(d)
|
||||
if parent == d:
|
||||
break
|
||||
d = parent
|
||||
return None
|
||||
|
||||
|
||||
def _sg_get_edit_mode(cfg_dir):
|
||||
try:
|
||||
pj = _sg_find_v8project(os.getcwd()) or _sg_find_v8project(cfg_dir)
|
||||
if not pj:
|
||||
return "deny"
|
||||
proj = json.loads(open(pj, encoding="utf-8-sig").read())
|
||||
cfg_full = os.path.normcase(os.path.abspath(cfg_dir)).rstrip("\\/")
|
||||
for db in proj.get("databases", []):
|
||||
src = db.get("configSrc")
|
||||
if src:
|
||||
src_full = os.path.normcase(os.path.abspath(src)).rstrip("\\/")
|
||||
if cfg_full == src_full or cfg_full.startswith(src_full + os.sep):
|
||||
if db.get("editingAllowedCheck"):
|
||||
return db["editingAllowedCheck"]
|
||||
if proj.get("editingAllowedCheck"):
|
||||
return proj["editingAllowedCheck"]
|
||||
return "deny"
|
||||
except Exception:
|
||||
return "deny"
|
||||
|
||||
|
||||
def assert_edit_allowed(target_path, require):
|
||||
try:
|
||||
rp = os.path.abspath(target_path)
|
||||
elem_uuid = _sg_root_uuid(rp)
|
||||
cfg_dir = None
|
||||
bin_path = None
|
||||
d = rp if os.path.isdir(rp) else os.path.dirname(rp)
|
||||
for _ in range(12):
|
||||
if not d:
|
||||
break
|
||||
if not elem_uuid:
|
||||
elem_uuid = _sg_root_uuid(d + ".xml")
|
||||
if not cfg_dir:
|
||||
cand = os.path.join(d, "Ext", "ParentConfigurations.bin")
|
||||
if os.path.exists(cand) or os.path.exists(os.path.join(d, "Configuration.xml")):
|
||||
cfg_dir = d
|
||||
bin_path = cand
|
||||
if elem_uuid and cfg_dir:
|
||||
break
|
||||
parent = os.path.dirname(d)
|
||||
if parent == d:
|
||||
break
|
||||
d = parent
|
||||
if not elem_uuid and cfg_dir:
|
||||
elem_uuid = _sg_root_uuid(os.path.join(cfg_dir, "Configuration.xml"))
|
||||
if not bin_path or not os.path.exists(bin_path):
|
||||
return
|
||||
data = open(bin_path, "rb").read()
|
||||
if len(data) <= 32:
|
||||
return
|
||||
if data[:3] == b"\xef\xbb\xbf":
|
||||
data = data[3:]
|
||||
text = data.decode("utf-8", "replace")
|
||||
h = re.match(r"\{6,(\d+),(\d+),", text)
|
||||
if not h:
|
||||
return
|
||||
g = int(h.group(1))
|
||||
k = int(h.group(2))
|
||||
if k == 0:
|
||||
return
|
||||
best = None
|
||||
if elem_uuid:
|
||||
for m in re.finditer(r"([0-2]),0," + re.escape(elem_uuid.lower()), text):
|
||||
f1 = int(m.group(1))
|
||||
if best is None or f1 < best:
|
||||
best = f1
|
||||
blocked = False
|
||||
code = ""
|
||||
reason = ""
|
||||
if g == 1:
|
||||
blocked = True
|
||||
code = "capability-off"
|
||||
reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)"
|
||||
elif require == "removed":
|
||||
if best is not None and best != 2:
|
||||
blocked = True
|
||||
code = "not-removed"
|
||||
reason = "объект не снят с поддержки — удаление сломает обновления"
|
||||
else:
|
||||
if best is not None and best == 0:
|
||||
blocked = True
|
||||
code = "locked"
|
||||
reason = "объект на замке — редактирование сломает обновления"
|
||||
if not blocked:
|
||||
return
|
||||
mode = _sg_get_edit_mode(cfg_dir)
|
||||
if mode == "off":
|
||||
return
|
||||
if mode == "warn":
|
||||
sys.stderr.write(f"[support-guard] ПРЕДУПРЕЖДЕНИЕ: {reason}. Цель: {rp}\n")
|
||||
return
|
||||
head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления."
|
||||
cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются."
|
||||
off_note = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json."
|
||||
if code == "capability-off":
|
||||
state = f"Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «{rp}» редактировать нельзя."
|
||||
fix = (
|
||||
"Либо снять защиту явно (навык support-edit, два шага):\n"
|
||||
f' 1. support-edit -Path "{cfg_dir}" -Capability on — включить возможность изменения (объекты пока остаются на замке);\n'
|
||||
f' 2. support-edit -Path "{rp}" -Set editable — открыть этот объект для редактирования.\n'
|
||||
" Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора."
|
||||
)
|
||||
elif code == "not-removed":
|
||||
state = f"Состояние: объект «{rp}» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора."
|
||||
fix = (
|
||||
"Либо сначала снять объект с поддержки, затем удалять:\n"
|
||||
f' support-edit -Path "{rp}" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно.'
|
||||
)
|
||||
else:
|
||||
state = f"Состояние: объект «{rp}» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)."
|
||||
fix = (
|
||||
"Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):\n"
|
||||
f' support-edit -Path "{rp}" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);\n'
|
||||
f' support-edit -Path "{rp}" -Set off-support — снять с поддержки: обновления по объекту больше не приходят.'
|
||||
)
|
||||
sys.stderr.write(head + "\n" + state + "\n" + cfe + "\n" + fix + "\n" + off_note + "\n")
|
||||
sys.exit(1)
|
||||
except SystemExit:
|
||||
raise
|
||||
except Exception:
|
||||
return
|
||||
|
||||
|
||||
MD_NS = "http://v8.1c.ru/8.3/MDClasses"
|
||||
XR_NS = "http://v8.1c.ru/8.3/xcf/readable"
|
||||
XSI_NS = "http://www.w3.org/2001/XMLSchema-instance"
|
||||
@@ -190,6 +351,8 @@ def main():
|
||||
resolved_path = os.path.abspath(config_path)
|
||||
config_dir = os.path.dirname(resolved_path)
|
||||
|
||||
assert_edit_allowed(resolved_path, "editable")
|
||||
|
||||
xml_parser = etree.XMLParser(remove_blank_text=False)
|
||||
tree = etree.parse(resolved_path, xml_parser)
|
||||
xml_root = tree.getroot()
|
||||
@@ -806,7 +969,7 @@ def main():
|
||||
if os.path.isfile(validate_script):
|
||||
print()
|
||||
print("--- Running cf-validate ---")
|
||||
subprocess.run([sys.executable, validate_script, "-ConfigPath", "-Path", resolved_path])
|
||||
subprocess.run([sys.executable, validate_script, "-ConfigPath", resolved_path])
|
||||
|
||||
# --- Summary ---
|
||||
print()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# cf-info v1.2 — Compact summary of 1C configuration root
|
||||
# cf-info v1.3 — Compact summary of 1C configuration root
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][Alias('Path')][string]$ConfigPath,
|
||||
@@ -218,6 +218,78 @@ function Get-HomePageLayout {
|
||||
|
||||
$script:homePage = Get-HomePageLayout
|
||||
|
||||
# --- Support state (Ext/ParentConfigurations.bin) ---
|
||||
# Decodes the 1C support-state file. See docs/1c-support-state-spec.md.
|
||||
# Returns $null on absent/error; else hashtable: State='absent'|'removed'|'parsed',
|
||||
# G (0=editing on, 1=off), K (vendor configs), Vendors @(@{Vendor;Name;Version}),
|
||||
# Counts @(locked, editable, removed) by f1 — record tally (K>1 counts each
|
||||
# vendor block separately); only computed when G=0.
|
||||
function Read-SupportState([string]$binPath) {
|
||||
try {
|
||||
if (-not (Test-Path $binPath)) { return @{ State = 'absent' } }
|
||||
$bytes = [System.IO.File]::ReadAllBytes($binPath)
|
||||
if ($bytes.Length -le 32) { return @{ State = 'removed' } }
|
||||
$startIdx = 0
|
||||
if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $startIdx = 3 }
|
||||
$text = [System.Text.Encoding]::UTF8.GetString($bytes, $startIdx, $bytes.Length - $startIdx)
|
||||
$h = [regex]::Match($text, '^\{6,(\d+),(\d+),')
|
||||
if (-not $h.Success) { return $null }
|
||||
$G = [int]$h.Groups[1].Value
|
||||
$K = [int]$h.Groups[2].Value
|
||||
if ($K -eq 0) { return @{ State = 'removed' } }
|
||||
# Vendor descriptors: ...,"ver","vendor","name",count,
|
||||
$vendors = @()
|
||||
$vRe = [regex]'"((?:[^"]|"")*)","((?:[^"]|"")*)","((?:[^"]|"")*)",\d+,'
|
||||
foreach ($m in $vRe.Matches($text)) {
|
||||
$vendors += @{
|
||||
Version = ($m.Groups[1].Value -replace '""','"')
|
||||
Vendor = ($m.Groups[2].Value -replace '""','"')
|
||||
Name = ($m.Groups[3].Value -replace '""','"')
|
||||
}
|
||||
}
|
||||
# Per-object counts only matter when editing is enabled (G=0); when G=1 the
|
||||
# whole config is read-only and stored f1 values are the inactive default.
|
||||
$counts = $null
|
||||
if ($G -eq 0) {
|
||||
$counts = @(0, 0, 0)
|
||||
# Object records: f1,0,uuidLocal[,uuidVendor] — flags precede the uuid.
|
||||
$rRe = [regex]'([0-2]),0,[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'
|
||||
foreach ($m in $rRe.Matches($text)) {
|
||||
$counts[[int]$m.Groups[1].Value]++
|
||||
}
|
||||
}
|
||||
return @{ State = 'parsed'; G = $G; K = $K; Vendors = $vendors; Counts = $counts }
|
||||
} catch { return $null }
|
||||
}
|
||||
|
||||
function Get-SupportLines {
|
||||
$configDir = [System.IO.Path]::GetDirectoryName($ConfigPath)
|
||||
$binPath = Join-Path (Join-Path $configDir "Ext") "ParentConfigurations.bin"
|
||||
$st = Read-SupportState $binPath
|
||||
$out = @()
|
||||
if (-not $st -or $st.State -eq 'absent') {
|
||||
if ($cfgExtPurpose) { $out += "Поддержка: расширение (CFE), правки свободны" }
|
||||
else { $out += "Поддержка: не на поддержке (своя конфигурация)" }
|
||||
return $out
|
||||
}
|
||||
if ($st.State -eq 'removed') {
|
||||
$out += "Поддержка: снята с поддержки полностью"
|
||||
return $out
|
||||
}
|
||||
$out += "Поддержка: на поддержке"
|
||||
if ($st.G -eq 0) {
|
||||
$out += " Возможность изменения: включена"
|
||||
$out += " Объектов: на замке $($st.Counts[0]) / редактируется $($st.Counts[1]) / снято $($st.Counts[2])"
|
||||
} else {
|
||||
$out += " Возможность изменения: выключена — вся конфигурация read-only (правки заблокированы)"
|
||||
}
|
||||
$out += " Конфигураций поставщика: $($st.K)"
|
||||
if ($st.K -gt 1) {
|
||||
foreach ($v in $st.Vendors) { $out += " Поставщик: $($v.Vendor) — $($v.Name) $($v.Version)" }
|
||||
}
|
||||
return $out
|
||||
}
|
||||
|
||||
function Format-HomePageItem($it, [bool]$detailed) {
|
||||
$badges = @()
|
||||
$badges += "h=$($it.height)"
|
||||
@@ -253,6 +325,7 @@ $cfgVersion = Get-PropText "Version"
|
||||
$cfgVendor = Get-PropText "Vendor"
|
||||
$cfgCompat = Get-PropText "CompatibilityMode"
|
||||
$cfgExtCompat = Get-PropText "ConfigurationExtensionCompatibilityMode"
|
||||
$cfgExtPurpose = Get-PropText "ConfigurationExtensionPurpose"
|
||||
$cfgDefaultRun = Get-PropText "DefaultRunMode"
|
||||
$cfgScript = Get-PropText "ScriptVariant"
|
||||
$cfgDefaultLang = Get-PropText "DefaultLanguage"
|
||||
@@ -284,6 +357,7 @@ if ($Mode -eq "overview" -and -not $Section) {
|
||||
Out "Формат: $version"
|
||||
if ($cfgVendor) { Out "Поставщик: $cfgVendor" }
|
||||
if ($cfgVersion) { Out "Версия: $cfgVersion" }
|
||||
foreach ($l in (Get-SupportLines)) { Out $l }
|
||||
Out "Совместимость: $cfgCompat"
|
||||
Out "Режим запуска: $cfgDefaultRun"
|
||||
Out "Язык скриптов: $cfgScript"
|
||||
@@ -386,6 +460,7 @@ if ($Mode -eq "full" -and -not $Section) {
|
||||
if ($cfgPrefix) { Out "Префикс: $cfgPrefix" }
|
||||
if ($cfgVendor) { Out "Поставщик: $cfgVendor" }
|
||||
if ($cfgVersion) { Out "Версия: $cfgVersion" }
|
||||
foreach ($l in (Get-SupportLines)) { Out $l }
|
||||
$cfgUpdateAddr = Get-PropText "UpdateCatalogAddress"
|
||||
if ($cfgUpdateAddr) { Out "Каталог обн.: $cfgUpdateAddr" }
|
||||
Out ""
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
#!/usr/bin/env python3
|
||||
# cf-info v1.2 — Compact summary of 1C configuration root
|
||||
# cf-info v1.3 — Compact summary of 1C configuration root
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from collections import OrderedDict
|
||||
from lxml import etree
|
||||
@@ -219,6 +220,71 @@ def get_home_page_layout():
|
||||
|
||||
home_page = get_home_page_layout()
|
||||
|
||||
# --- Support state (Ext/ParentConfigurations.bin) ---
|
||||
# Decodes the 1C support-state file. See docs/1c-support-state-spec.md.
|
||||
# Returns None on absent/error; else dict: state='absent'|'removed'|'parsed',
|
||||
# g (0=editing on, 1=off), k (vendor configs), vendors [{vendor,name,version}],
|
||||
# counts [locked, editable, removed] by f1 — record tally (k>1 counts each
|
||||
# vendor block separately); only computed when g==0.
|
||||
def read_support_state(bin_path):
|
||||
try:
|
||||
if not os.path.isfile(bin_path):
|
||||
return {"state": "absent"}
|
||||
data = open(bin_path, "rb").read()
|
||||
if len(data) <= 32:
|
||||
return {"state": "removed"}
|
||||
if data[:3] == b"\xef\xbb\xbf":
|
||||
data = data[3:]
|
||||
text = data.decode("utf-8", "replace")
|
||||
h = re.match(r"\{6,(\d+),(\d+),", text)
|
||||
if not h:
|
||||
return None
|
||||
g = int(h.group(1))
|
||||
k = int(h.group(2))
|
||||
if k == 0:
|
||||
return {"state": "removed"}
|
||||
vendors = []
|
||||
for m in re.finditer(r'"((?:[^"]|"")*)","((?:[^"]|"")*)","((?:[^"]|"")*)",\d+,', text):
|
||||
vendors.append({
|
||||
"version": m.group(1).replace('""', '"'),
|
||||
"vendor": m.group(2).replace('""', '"'),
|
||||
"name": m.group(3).replace('""', '"'),
|
||||
})
|
||||
counts = None
|
||||
if g == 0:
|
||||
counts = [0, 0, 0]
|
||||
for m in re.finditer(r"([0-2]),0,[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", text):
|
||||
counts[int(m.group(1))] += 1
|
||||
return {"state": "parsed", "g": g, "k": k, "vendors": vendors, "counts": counts}
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_support_lines():
|
||||
config_dir = os.path.dirname(config_path)
|
||||
bin_path = os.path.join(config_dir, "Ext", "ParentConfigurations.bin")
|
||||
st = read_support_state(bin_path)
|
||||
res = []
|
||||
if not st or st["state"] == "absent":
|
||||
if cfg_ext_purpose:
|
||||
res.append("Поддержка: расширение (CFE), правки свободны")
|
||||
else:
|
||||
res.append("Поддержка: не на поддержке (своя конфигурация)")
|
||||
return res
|
||||
if st["state"] == "removed":
|
||||
res.append("Поддержка: снята с поддержки полностью")
|
||||
return res
|
||||
res.append("Поддержка: на поддержке")
|
||||
if st["g"] == 0:
|
||||
res.append(" Возможность изменения: включена")
|
||||
res.append(f" Объектов: на замке {st['counts'][0]} / редактируется {st['counts'][1]} / снято {st['counts'][2]}")
|
||||
else:
|
||||
res.append(" Возможность изменения: выключена — вся конфигурация read-only (правки заблокированы)")
|
||||
res.append(f" Конфигураций поставщика: {st['k']}")
|
||||
if st["k"] > 1:
|
||||
for v in st["vendors"]:
|
||||
res.append(f" Поставщик: {v['vendor']} — {v['name']} {v['version']}")
|
||||
return res
|
||||
|
||||
def format_home_page_item(it, detailed):
|
||||
badges = [f"h={it['height']}"]
|
||||
if not it["common"]:
|
||||
@@ -249,6 +315,7 @@ cfg_version = get_prop_text("Version")
|
||||
cfg_vendor = get_prop_text("Vendor")
|
||||
cfg_compat = get_prop_text("CompatibilityMode")
|
||||
cfg_ext_compat = get_prop_text("ConfigurationExtensionCompatibilityMode")
|
||||
cfg_ext_purpose = get_prop_text("ConfigurationExtensionPurpose")
|
||||
cfg_default_run = get_prop_text("DefaultRunMode")
|
||||
cfg_script = get_prop_text("ScriptVariant")
|
||||
cfg_default_lang = get_prop_text("DefaultLanguage")
|
||||
@@ -281,6 +348,8 @@ if args.Mode == "overview" and not args.Section:
|
||||
out(f"Поставщик: {cfg_vendor}")
|
||||
if cfg_version:
|
||||
out(f"Версия: {cfg_version}")
|
||||
for ln in get_support_lines():
|
||||
out(ln)
|
||||
out(f"Совместимость: {cfg_compat}")
|
||||
out(f"Режим запуска: {cfg_default_run}")
|
||||
out(f"Язык скриптов: {cfg_script}")
|
||||
@@ -369,6 +438,8 @@ if args.Mode == "full" and not args.Section:
|
||||
out(f"Поставщик: {cfg_vendor}")
|
||||
if cfg_version:
|
||||
out(f"Версия: {cfg_version}")
|
||||
for ln in get_support_lines():
|
||||
out(ln)
|
||||
cfg_update_addr = get_prop_text("UpdateCatalogAddress")
|
||||
if cfg_update_addr:
|
||||
out(f"Каталог обн.: {cfg_update_addr}")
|
||||
|
||||
@@ -48,17 +48,9 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" <п
|
||||
|
||||
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
||||
|
||||
## Коды возврата
|
||||
|
||||
| Код | Описание |
|
||||
|-----|----------|
|
||||
| 0 | Успешно |
|
||||
| 1 | Ошибка (см. лог) |
|
||||
|
||||
## После создания
|
||||
|
||||
1. Прочитай лог-файл и покажи результат
|
||||
2. Предложи зарегистрировать базу в `.v8-project.json` (через `/db-list add`)
|
||||
Предложи зарегистрировать базу в `.v8-project.json` (через `/db-list add`)
|
||||
3. Если указан шаблон `/UseTemplate` — предупреди что конфигурация будет загружена из шаблона
|
||||
|
||||
## Примеры
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# db-create v1.0 — Create 1C information base
|
||||
# db-create v1.1 — Create 1C information base
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
<#
|
||||
.SYNOPSIS
|
||||
@@ -67,15 +67,40 @@ $OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# --- Resolve V8Path ---
|
||||
function Find-ProjectV8Path {
|
||||
$dir = (Get-Location).Path
|
||||
while ($dir) {
|
||||
$pf = Join-Path $dir ".v8-project.json"
|
||||
if (Test-Path $pf) {
|
||||
try {
|
||||
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||
if ($j.v8path) { return [string]$j.v8path }
|
||||
} catch {}
|
||||
return $null
|
||||
}
|
||||
$parent = Split-Path $dir -Parent
|
||||
if (-not $parent -or $parent -eq $dir) { break }
|
||||
$dir = $parent
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
if (-not $V8Path) {
|
||||
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
|
||||
$V8Path = Find-ProjectV8Path
|
||||
}
|
||||
if (-not $V8Path) {
|
||||
$found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
|
||||
Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
|
||||
Select-Object -First 1
|
||||
if ($found) {
|
||||
$V8Path = $found.FullName
|
||||
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
||||
} else {
|
||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
} elseif (Test-Path $V8Path -PathType Container) {
|
||||
}
|
||||
if (Test-Path $V8Path -PathType Container) {
|
||||
$V8Path = Join-Path $V8Path "1cv8.exe"
|
||||
}
|
||||
|
||||
|
||||
@@ -1,29 +1,64 @@
|
||||
#!/usr/bin/env python3
|
||||
# db-create v1.0 — Create 1C information base
|
||||
# db-create v1.1 — Create 1C information base
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
|
||||
def _find_project_v8path():
|
||||
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
||||
d = os.getcwd()
|
||||
while True:
|
||||
pf = os.path.join(d, ".v8-project.json")
|
||||
if os.path.isfile(pf):
|
||||
try:
|
||||
with open(pf, encoding="utf-8-sig") as f:
|
||||
data = json.load(f)
|
||||
v = data.get("v8path")
|
||||
if v:
|
||||
return v
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
parent = os.path.dirname(d)
|
||||
if parent == d:
|
||||
return None
|
||||
d = parent
|
||||
|
||||
|
||||
def _version_key(p):
|
||||
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
|
||||
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
|
||||
return [int(x) for x in re.findall(r"\d+", ver)]
|
||||
|
||||
|
||||
def resolve_v8path(v8path):
|
||||
"""Resolve path to 1cv8.exe."""
|
||||
if not v8path:
|
||||
found = sorted(glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe"))
|
||||
if found:
|
||||
return found[-1]
|
||||
v8path = _find_project_v8path()
|
||||
if not v8path:
|
||||
candidates = (
|
||||
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
||||
)
|
||||
if candidates:
|
||||
v8path = max(candidates, key=_version_key)
|
||||
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
|
||||
print(f"Auto-selected platform {ver}: {v8path}")
|
||||
else:
|
||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
elif os.path.isdir(v8path):
|
||||
if os.path.isdir(v8path):
|
||||
v8path = os.path.join(v8path, "1cv8.exe")
|
||||
|
||||
if not os.path.isfile(v8path):
|
||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
@@ -54,17 +54,6 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.ps1" <п
|
||||
|
||||
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
||||
|
||||
## Коды возврата
|
||||
|
||||
| Код | Описание |
|
||||
|-----|----------|
|
||||
| 0 | Успешно |
|
||||
| 1 | Ошибка (см. лог) |
|
||||
|
||||
## После выполнения
|
||||
|
||||
Прочитай лог-файл и покажи результат. Если есть ошибки — покажи содержимое лога.
|
||||
|
||||
## Примеры
|
||||
|
||||
```powershell
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# db-dump-cf v1.0 — Dump 1C configuration to CF file
|
||||
# db-dump-cf v1.1 — Dump 1C configuration to CF file
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
<#
|
||||
.SYNOPSIS
|
||||
@@ -76,15 +76,40 @@ $OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# --- Resolve V8Path ---
|
||||
function Find-ProjectV8Path {
|
||||
$dir = (Get-Location).Path
|
||||
while ($dir) {
|
||||
$pf = Join-Path $dir ".v8-project.json"
|
||||
if (Test-Path $pf) {
|
||||
try {
|
||||
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||
if ($j.v8path) { return [string]$j.v8path }
|
||||
} catch {}
|
||||
return $null
|
||||
}
|
||||
$parent = Split-Path $dir -Parent
|
||||
if (-not $parent -or $parent -eq $dir) { break }
|
||||
$dir = $parent
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
if (-not $V8Path) {
|
||||
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
|
||||
$V8Path = Find-ProjectV8Path
|
||||
}
|
||||
if (-not $V8Path) {
|
||||
$found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
|
||||
Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
|
||||
Select-Object -First 1
|
||||
if ($found) {
|
||||
$V8Path = $found.FullName
|
||||
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
||||
} else {
|
||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
} elseif (Test-Path $V8Path -PathType Container) {
|
||||
}
|
||||
if (Test-Path $V8Path -PathType Container) {
|
||||
$V8Path = Join-Path $V8Path "1cv8.exe"
|
||||
}
|
||||
|
||||
|
||||
@@ -1,29 +1,64 @@
|
||||
#!/usr/bin/env python3
|
||||
# db-dump-cf v1.0 — Dump 1C configuration to CF file
|
||||
# db-dump-cf v1.1 — Dump 1C configuration to CF file
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
|
||||
def _find_project_v8path():
|
||||
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
||||
d = os.getcwd()
|
||||
while True:
|
||||
pf = os.path.join(d, ".v8-project.json")
|
||||
if os.path.isfile(pf):
|
||||
try:
|
||||
with open(pf, encoding="utf-8-sig") as f:
|
||||
data = json.load(f)
|
||||
v = data.get("v8path")
|
||||
if v:
|
||||
return v
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
parent = os.path.dirname(d)
|
||||
if parent == d:
|
||||
return None
|
||||
d = parent
|
||||
|
||||
|
||||
def _version_key(p):
|
||||
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
|
||||
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
|
||||
return [int(x) for x in re.findall(r"\d+", ver)]
|
||||
|
||||
|
||||
def resolve_v8path(v8path):
|
||||
"""Resolve path to 1cv8.exe."""
|
||||
if not v8path:
|
||||
found = sorted(glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe"))
|
||||
if found:
|
||||
return found[-1]
|
||||
v8path = _find_project_v8path()
|
||||
if not v8path:
|
||||
candidates = (
|
||||
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
||||
)
|
||||
if candidates:
|
||||
v8path = max(candidates, key=_version_key)
|
||||
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
|
||||
print(f"Auto-selected platform {ver}: {v8path}")
|
||||
else:
|
||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
elif os.path.isdir(v8path):
|
||||
if os.path.isdir(v8path):
|
||||
v8path = os.path.join(v8path, "1cv8.exe")
|
||||
|
||||
if not os.path.isfile(v8path):
|
||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
---
|
||||
name: db-dump-dt
|
||||
description: Выгрузка информационной базы 1С в DT-файл (вся база — конфигурация + данные). Используй когда нужно выгрузить информационную базу, выгрузить архив базы, сделать бэкап, выгрузить dt
|
||||
argument-hint: "[database] [output.dt]"
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Glob
|
||||
- AskUserQuestion
|
||||
---
|
||||
|
||||
# /db-dump-dt — Выгрузка информационной базы в DT-файл
|
||||
|
||||
Выгружает информационную базу целиком (конфигурация **+ данные**) в DT-файл — полный снимок ИБ.
|
||||
|
||||
> В отличие от `/db-dump-cf` (только конфигурация), `.dt` содержит **всю базу**: данные,
|
||||
> настройки, пользователей. Это бэкап/точка отката, а не выгрузка метаданных.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/db-dump-dt [database] [output.dt]
|
||||
/db-dump-dt dev backup.dt
|
||||
/db-dump-dt — база по умолчанию, имя файла по базе и дате
|
||||
```
|
||||
|
||||
## Параметры подключения
|
||||
|
||||
Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу:
|
||||
1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
|
||||
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
||||
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
||||
4. Если ветка не совпала — используй `default`
|
||||
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1`
|
||||
Если файла нет — предложи `/db-list add`.
|
||||
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
||||
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-dt.ps1" <параметры>
|
||||
```
|
||||
|
||||
### Параметры скрипта
|
||||
|
||||
| Параметр | Обязательный | Описание |
|
||||
|----------|:------------:|----------|
|
||||
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
|
||||
| `-InfoBasePath <путь>` | * | Файловая база |
|
||||
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
||||
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
||||
| `-UserName <имя>` | нет | Имя пользователя |
|
||||
| `-Password <пароль>` | нет | Пароль |
|
||||
| `-OutputFile <путь>` | да | Путь к выходному DT-файлу |
|
||||
|
||||
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
||||
|
||||
## Примеры
|
||||
|
||||
```powershell
|
||||
# Выгрузка ИБ (файловая база)
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-dt.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "C:\backup\base.dt"
|
||||
|
||||
# Серверная база
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-dt.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -OutputFile "base.dt"
|
||||
```
|
||||
|
||||
## Связанные навыки
|
||||
|
||||
- `/db-load-dt` — загрузка ИБ из DT (обратная операция)
|
||||
- `/db-dump-cf` — выгрузка только конфигурации (без данных)
|
||||
- `/db-create` — создать новую базу (в т.ч. из DT-шаблона)
|
||||
@@ -0,0 +1,168 @@
|
||||
# db-dump-dt v1.1 — Dump 1C information base to DT file
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Выгрузка информационной базы 1С в DT-файл
|
||||
|
||||
.DESCRIPTION
|
||||
Выгружает информационную базу целиком (конфигурация + данные) в DT-файл.
|
||||
|
||||
.PARAMETER V8Path
|
||||
Путь к каталогу bin платформы или к 1cv8.exe
|
||||
|
||||
.PARAMETER InfoBasePath
|
||||
Путь к файловой информационной базе
|
||||
|
||||
.PARAMETER InfoBaseServer
|
||||
Сервер 1С (для серверной базы)
|
||||
|
||||
.PARAMETER InfoBaseRef
|
||||
Имя базы на сервере
|
||||
|
||||
.PARAMETER UserName
|
||||
Имя пользователя 1С
|
||||
|
||||
.PARAMETER Password
|
||||
Пароль пользователя
|
||||
|
||||
.PARAMETER OutputFile
|
||||
Путь к выходному DT-файлу
|
||||
|
||||
.EXAMPLE
|
||||
.\db-dump-dt.ps1 -InfoBasePath "C:\Bases\MyDB" -OutputFile "backup.dt"
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$V8Path,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$InfoBasePath,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$InfoBaseServer,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$InfoBaseRef,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$UserName,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$Password,
|
||||
|
||||
[Parameter(Mandatory=$true)]
|
||||
[string]$OutputFile
|
||||
)
|
||||
|
||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# --- Resolve V8Path ---
|
||||
function Find-ProjectV8Path {
|
||||
$dir = (Get-Location).Path
|
||||
while ($dir) {
|
||||
$pf = Join-Path $dir ".v8-project.json"
|
||||
if (Test-Path $pf) {
|
||||
try {
|
||||
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||
if ($j.v8path) { return [string]$j.v8path }
|
||||
} catch {}
|
||||
return $null
|
||||
}
|
||||
$parent = Split-Path $dir -Parent
|
||||
if (-not $parent -or $parent -eq $dir) { break }
|
||||
$dir = $parent
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
if (-not $V8Path) {
|
||||
$V8Path = Find-ProjectV8Path
|
||||
}
|
||||
if (-not $V8Path) {
|
||||
$found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
|
||||
Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
|
||||
Select-Object -First 1
|
||||
if ($found) {
|
||||
$V8Path = $found.FullName
|
||||
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
||||
} else {
|
||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
if (Test-Path $V8Path -PathType Container) {
|
||||
$V8Path = Join-Path $V8Path "1cv8.exe"
|
||||
}
|
||||
|
||||
if (-not (Test-Path $V8Path)) {
|
||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Validate connection ---
|
||||
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
||||
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Ensure output directory exists ---
|
||||
$outDir = Split-Path $OutputFile -Parent
|
||||
if ($outDir -and -not (Test-Path $outDir)) {
|
||||
New-Item -ItemType Directory -Path $outDir -Force | Out-Null
|
||||
}
|
||||
|
||||
# --- Temp dir ---
|
||||
$tempDir = Join-Path $env:TEMP "db_dump_dt_$(Get-Random)"
|
||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
||||
|
||||
try {
|
||||
# --- Build arguments ---
|
||||
$arguments = @("DESIGNER")
|
||||
|
||||
if ($InfoBaseServer -and $InfoBaseRef) {
|
||||
$arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`""
|
||||
} else {
|
||||
$arguments += "/F", "`"$InfoBasePath`""
|
||||
}
|
||||
|
||||
if ($UserName) { $arguments += "/N`"$UserName`"" }
|
||||
if ($Password) { $arguments += "/P`"$Password`"" }
|
||||
|
||||
$arguments += "/DumpIB", "`"$OutputFile`""
|
||||
|
||||
# --- Output ---
|
||||
$outFile = Join-Path $tempDir "dump_dt_log.txt"
|
||||
$arguments += "/Out", "`"$outFile`""
|
||||
$arguments += "/DisableStartupDialogs"
|
||||
|
||||
# --- Execute ---
|
||||
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
|
||||
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
||||
$exitCode = $process.ExitCode
|
||||
|
||||
# --- Result ---
|
||||
if ($exitCode -eq 0) {
|
||||
Write-Host "Information base dumped successfully to: $OutputFile" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "Error dumping information base (code: $exitCode)" -ForegroundColor Red
|
||||
}
|
||||
|
||||
if (Test-Path $outFile) {
|
||||
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
|
||||
if ($logContent) {
|
||||
Write-Host "--- Log ---"
|
||||
Write-Host $logContent
|
||||
Write-Host "--- End ---"
|
||||
}
|
||||
}
|
||||
|
||||
exit $exitCode
|
||||
|
||||
} finally {
|
||||
if (Test-Path $tempDir) {
|
||||
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
#!/usr/bin/env python3
|
||||
# db-dump-dt v1.1 — Dump 1C information base to DT file
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
|
||||
def _find_project_v8path():
|
||||
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
||||
d = os.getcwd()
|
||||
while True:
|
||||
pf = os.path.join(d, ".v8-project.json")
|
||||
if os.path.isfile(pf):
|
||||
try:
|
||||
with open(pf, encoding="utf-8-sig") as f:
|
||||
data = json.load(f)
|
||||
v = data.get("v8path")
|
||||
if v:
|
||||
return v
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
parent = os.path.dirname(d)
|
||||
if parent == d:
|
||||
return None
|
||||
d = parent
|
||||
|
||||
|
||||
def _version_key(p):
|
||||
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
|
||||
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
|
||||
return [int(x) for x in re.findall(r"\d+", ver)]
|
||||
|
||||
|
||||
def resolve_v8path(v8path):
|
||||
"""Resolve path to 1cv8.exe."""
|
||||
if not v8path:
|
||||
v8path = _find_project_v8path()
|
||||
if not v8path:
|
||||
candidates = (
|
||||
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
||||
)
|
||||
if candidates:
|
||||
v8path = max(candidates, key=_version_key)
|
||||
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
|
||||
print(f"Auto-selected platform {ver}: {v8path}")
|
||||
else:
|
||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
if os.path.isdir(v8path):
|
||||
v8path = os.path.join(v8path, "1cv8.exe")
|
||||
if not os.path.isfile(v8path):
|
||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
return v8path
|
||||
|
||||
|
||||
def main():
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
sys.stderr.reconfigure(encoding="utf-8")
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Dump 1C information base to DT file",
|
||||
allow_abbrev=False,
|
||||
)
|
||||
parser.add_argument("-V8Path", default="")
|
||||
parser.add_argument("-InfoBasePath", default="")
|
||||
parser.add_argument("-InfoBaseServer", default="")
|
||||
parser.add_argument("-InfoBaseRef", default="")
|
||||
parser.add_argument("-UserName", default="")
|
||||
parser.add_argument("-Password", default="")
|
||||
parser.add_argument("-OutputFile", required=True)
|
||||
args = parser.parse_args()
|
||||
|
||||
v8path = resolve_v8path(args.V8Path)
|
||||
|
||||
# --- Validate connection ---
|
||||
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# --- Ensure output directory exists ---
|
||||
out_dir = os.path.dirname(args.OutputFile)
|
||||
if out_dir and not os.path.isdir(out_dir):
|
||||
os.makedirs(out_dir, exist_ok=True)
|
||||
|
||||
# --- Temp dir ---
|
||||
temp_dir = os.path.join(tempfile.gettempdir(), f"db_dump_dt_{random.randint(0, 999999)}")
|
||||
os.makedirs(temp_dir, exist_ok=True)
|
||||
|
||||
try:
|
||||
# --- Build arguments ---
|
||||
arguments = ["DESIGNER"]
|
||||
|
||||
if args.InfoBaseServer and args.InfoBaseRef:
|
||||
arguments.extend(["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"])
|
||||
else:
|
||||
arguments.extend(["/F", args.InfoBasePath])
|
||||
|
||||
if args.UserName:
|
||||
arguments.append(f"/N{args.UserName}")
|
||||
if args.Password:
|
||||
arguments.append(f"/P{args.Password}")
|
||||
|
||||
arguments.extend(["/DumpIB", args.OutputFile])
|
||||
|
||||
# --- Output ---
|
||||
out_file = os.path.join(temp_dir, "dump_dt_log.txt")
|
||||
arguments.extend(["/Out", out_file])
|
||||
arguments.append("/DisableStartupDialogs")
|
||||
|
||||
# --- Execute ---
|
||||
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
||||
result = subprocess.run(
|
||||
[v8path] + arguments,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
exit_code = result.returncode
|
||||
|
||||
# --- Result ---
|
||||
if exit_code == 0:
|
||||
print(f"Information base dumped successfully to: {args.OutputFile}")
|
||||
else:
|
||||
print(f"Error dumping information base (code: {exit_code})", file=sys.stderr)
|
||||
|
||||
if os.path.isfile(out_file):
|
||||
try:
|
||||
with open(out_file, "r", encoding="utf-8-sig") as f:
|
||||
log_content = f.read()
|
||||
if log_content:
|
||||
print("--- Log ---")
|
||||
print(log_content)
|
||||
print("--- End ---")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
sys.exit(exit_code)
|
||||
|
||||
finally:
|
||||
if os.path.isdir(temp_dir):
|
||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -68,13 +68,6 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" <
|
||||
| `Partial` | Частичная — выбранные объекты из параметра `-Objects` |
|
||||
| `UpdateInfo` | Обновить только ConfigDumpInfo.xml без выгрузки файлов |
|
||||
|
||||
## Коды возврата
|
||||
|
||||
| Код | Описание |
|
||||
|-----|----------|
|
||||
| 0 | Успешно |
|
||||
| 1 | Ошибка (см. лог) |
|
||||
|
||||
> Если пользователь просит выгрузить конкретные объекты — используй `-Mode Partial` с `-Objects`.
|
||||
|
||||
## Примеры
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# db-dump-xml v1.0 — Dump 1C configuration to XML files
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
# db-dump-xml v1.1 — Dump 1C configuration to XML files
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Выгрузка конфигурации 1С в XML-файлы
|
||||
@@ -99,15 +99,40 @@ $OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# --- Resolve V8Path ---
|
||||
function Find-ProjectV8Path {
|
||||
$dir = (Get-Location).Path
|
||||
while ($dir) {
|
||||
$pf = Join-Path $dir ".v8-project.json"
|
||||
if (Test-Path $pf) {
|
||||
try {
|
||||
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||
if ($j.v8path) { return [string]$j.v8path }
|
||||
} catch {}
|
||||
return $null
|
||||
}
|
||||
$parent = Split-Path $dir -Parent
|
||||
if (-not $parent -or $parent -eq $dir) { break }
|
||||
$dir = $parent
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
if (-not $V8Path) {
|
||||
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
|
||||
$V8Path = Find-ProjectV8Path
|
||||
}
|
||||
if (-not $V8Path) {
|
||||
$found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
|
||||
Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
|
||||
Select-Object -First 1
|
||||
if ($found) {
|
||||
$V8Path = $found.FullName
|
||||
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
||||
} else {
|
||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
} elseif (Test-Path $V8Path -PathType Container) {
|
||||
}
|
||||
if (Test-Path $V8Path -PathType Container) {
|
||||
$V8Path = Join-Path $V8Path "1cv8.exe"
|
||||
}
|
||||
|
||||
|
||||
@@ -1,34 +1,67 @@
|
||||
#!/usr/bin/env python3
|
||||
# db-dump-xml v1.0 — Dump 1C configuration to XML files
|
||||
# db-dump-xml v1.1 — Dump 1C configuration to XML files
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
|
||||
def _find_project_v8path():
|
||||
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
||||
d = os.getcwd()
|
||||
while True:
|
||||
pf = os.path.join(d, ".v8-project.json")
|
||||
if os.path.isfile(pf):
|
||||
try:
|
||||
with open(pf, encoding="utf-8-sig") as f:
|
||||
data = json.load(f)
|
||||
v = data.get("v8path")
|
||||
if v:
|
||||
return v
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
parent = os.path.dirname(d)
|
||||
if parent == d:
|
||||
return None
|
||||
d = parent
|
||||
|
||||
|
||||
def _version_key(p):
|
||||
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
|
||||
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
|
||||
return [int(x) for x in re.findall(r"\d+", ver)]
|
||||
|
||||
|
||||
def resolve_v8path(v8path):
|
||||
"""Resolve path to 1cv8.exe."""
|
||||
if not v8path:
|
||||
candidates = glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||
v8path = _find_project_v8path()
|
||||
if not v8path:
|
||||
candidates = (
|
||||
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
||||
)
|
||||
if candidates:
|
||||
candidates.sort()
|
||||
return candidates[-1]
|
||||
v8path = max(candidates, key=_version_key)
|
||||
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
|
||||
print(f"Auto-selected platform {ver}: {v8path}")
|
||||
else:
|
||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
elif os.path.isdir(v8path):
|
||||
if os.path.isdir(v8path):
|
||||
v8path = os.path.join(v8path, "1cv8.exe")
|
||||
|
||||
if not os.path.isfile(v8path):
|
||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
return v8path
|
||||
|
||||
|
||||
|
||||
@@ -55,17 +55,9 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.ps1" <п
|
||||
|
||||
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
||||
|
||||
## Коды возврата
|
||||
|
||||
| Код | Описание |
|
||||
|-----|----------|
|
||||
| 0 | Успешно |
|
||||
| 1 | Ошибка (см. лог) |
|
||||
|
||||
## После выполнения
|
||||
|
||||
1. Прочитай лог-файл и покажи результат
|
||||
2. **Предложи выполнить `/db-update`** — загрузка CF обновляет только «основную» конфигурацию конфигуратора, для применения к БД нужен `/UpdateDBCfg`
|
||||
**Предложи выполнить `/db-update`** — загрузка CF обновляет только «основную» конфигурацию конфигуратора, для применения к БД нужен `/UpdateDBCfg`
|
||||
|
||||
## Примеры
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# db-load-cf v1.0 — Load 1C configuration from CF file
|
||||
# db-load-cf v1.1 — Load 1C configuration from CF file
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
<#
|
||||
.SYNOPSIS
|
||||
@@ -76,15 +76,40 @@ $OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# --- Resolve V8Path ---
|
||||
function Find-ProjectV8Path {
|
||||
$dir = (Get-Location).Path
|
||||
while ($dir) {
|
||||
$pf = Join-Path $dir ".v8-project.json"
|
||||
if (Test-Path $pf) {
|
||||
try {
|
||||
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||
if ($j.v8path) { return [string]$j.v8path }
|
||||
} catch {}
|
||||
return $null
|
||||
}
|
||||
$parent = Split-Path $dir -Parent
|
||||
if (-not $parent -or $parent -eq $dir) { break }
|
||||
$dir = $parent
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
if (-not $V8Path) {
|
||||
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
|
||||
$V8Path = Find-ProjectV8Path
|
||||
}
|
||||
if (-not $V8Path) {
|
||||
$found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
|
||||
Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
|
||||
Select-Object -First 1
|
||||
if ($found) {
|
||||
$V8Path = $found.FullName
|
||||
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
||||
} else {
|
||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
} elseif (Test-Path $V8Path -PathType Container) {
|
||||
}
|
||||
if (Test-Path $V8Path -PathType Container) {
|
||||
$V8Path = Join-Path $V8Path "1cv8.exe"
|
||||
}
|
||||
|
||||
|
||||
@@ -1,29 +1,64 @@
|
||||
#!/usr/bin/env python3
|
||||
# db-load-cf v1.0 — Load 1C configuration from CF file
|
||||
# db-load-cf v1.1 — Load 1C configuration from CF file
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
|
||||
def _find_project_v8path():
|
||||
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
||||
d = os.getcwd()
|
||||
while True:
|
||||
pf = os.path.join(d, ".v8-project.json")
|
||||
if os.path.isfile(pf):
|
||||
try:
|
||||
with open(pf, encoding="utf-8-sig") as f:
|
||||
data = json.load(f)
|
||||
v = data.get("v8path")
|
||||
if v:
|
||||
return v
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
parent = os.path.dirname(d)
|
||||
if parent == d:
|
||||
return None
|
||||
d = parent
|
||||
|
||||
|
||||
def _version_key(p):
|
||||
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
|
||||
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
|
||||
return [int(x) for x in re.findall(r"\d+", ver)]
|
||||
|
||||
|
||||
def resolve_v8path(v8path):
|
||||
"""Resolve path to 1cv8.exe."""
|
||||
if not v8path:
|
||||
found = sorted(glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe"))
|
||||
if found:
|
||||
return found[-1]
|
||||
v8path = _find_project_v8path()
|
||||
if not v8path:
|
||||
candidates = (
|
||||
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
||||
)
|
||||
if candidates:
|
||||
v8path = max(candidates, key=_version_key)
|
||||
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
|
||||
print(f"Auto-selected platform {ver}: {v8path}")
|
||||
else:
|
||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
elif os.path.isdir(v8path):
|
||||
if os.path.isdir(v8path):
|
||||
v8path = os.path.join(v8path, "1cv8.exe")
|
||||
|
||||
if not os.path.isfile(v8path):
|
||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
---
|
||||
name: db-load-dt
|
||||
description: Загрузка информационной базы 1С из DT-файла — полная перезапись базы (конфигурация + данные). Используй когда нужно загрузить архив информационной базы, восстановить базу, загрузить dt
|
||||
disable-model-invocation: true
|
||||
argument-hint: <input.dt> [database]
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Glob
|
||||
- AskUserQuestion
|
||||
---
|
||||
|
||||
# /db-load-dt — Загрузка информационной базы из DT-файла
|
||||
|
||||
Восстанавливает информационную базу целиком (конфигурация **+ данные**) из DT-файла.
|
||||
|
||||
> ⚠️ **Необратимая операция.** Загрузка `.dt` **полностью перезаписывает базу** — и
|
||||
> конфигурацию, и все данные. Текущее содержимое базы будет потеряно. После загрузки
|
||||
> `/db-update` **не нужен** — конфигурация БД уже синхронна внутри снимка.
|
||||
|
||||
## Когда НЕ использовать
|
||||
|
||||
- Нужно создать **новую** базу из `.dt` → используй `/db-create` (из DT-шаблона), а не загрузку
|
||||
в существующую.
|
||||
- Нужно обновить только конфигурацию (без данных) → `/db-load-cf` или `/db-load-xml`.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/db-load-dt <input.dt> [database]
|
||||
/db-load-dt backup.dt dev
|
||||
```
|
||||
|
||||
## Порядок действий перед загрузкой
|
||||
|
||||
1. Предложи пользователю сначала сделать `/db-dump-dt` текущего состояния базы — это точка
|
||||
отката (восстановиться будет нечем, если не сохранить).
|
||||
2. Запроси **явное подтверждение**: вся база (данные + конфигурация) будет перезаписана.
|
||||
3. Только после подтверждения выполняй загрузку.
|
||||
|
||||
## Параметры подключения
|
||||
|
||||
Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу:
|
||||
1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
|
||||
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
||||
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
||||
4. Если ветка не совпала — используй `default`
|
||||
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1`
|
||||
Если файла нет — предложи `/db-list add`.
|
||||
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
||||
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-dt.ps1" <параметры>
|
||||
```
|
||||
|
||||
### Параметры скрипта
|
||||
|
||||
| Параметр | Обязательный | Описание |
|
||||
|----------|:------------:|----------|
|
||||
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
|
||||
| `-InfoBasePath <путь>` | * | Файловая база |
|
||||
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
||||
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
||||
| `-UserName <имя>` | нет | Имя пользователя |
|
||||
| `-Password <пароль>` | нет | Пароль |
|
||||
| `-InputFile <путь>` | да | Путь к DT-файлу |
|
||||
| `-JobsCount <N>` | нет | Число фоновых заданий загрузки (0 = по числу процессоров) |
|
||||
| `-UnlockCode <код>` | нет | Код разблокировки (`/UC`), если заблокировано начало сеансов |
|
||||
|
||||
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
||||
|
||||
## После выполнения
|
||||
|
||||
Если база занята (активные сеансы), загрузка не выполнится — для серверной базы можно
|
||||
передать `-UnlockCode`; иначе освободи базу и повтори.
|
||||
|
||||
## Примеры
|
||||
|
||||
```powershell
|
||||
# Файловая база
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-dt.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "C:\backup\base.dt"
|
||||
|
||||
# Серверная база с ускорением загрузки
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-dt.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test" -UserName "Admin" -Password "secret" -InputFile "base.dt" -JobsCount 4
|
||||
```
|
||||
|
||||
## Связанные навыки
|
||||
|
||||
- `/db-dump-dt` — выгрузка ИБ в DT (обратная операция, точка отката перед загрузкой)
|
||||
- `/db-create` — создать новую базу (в т.ч. из DT-шаблона)
|
||||
@@ -0,0 +1,183 @@
|
||||
# db-load-dt v1.1 — Load 1C information base from DT file
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Загрузка информационной базы 1С из DT-файла
|
||||
|
||||
.DESCRIPTION
|
||||
Загружает информационную базу целиком (конфигурация + данные) из DT-файла.
|
||||
ВНИМАНИЕ: операция полностью перезаписывает базу.
|
||||
|
||||
.PARAMETER V8Path
|
||||
Путь к каталогу bin платформы или к 1cv8.exe
|
||||
|
||||
.PARAMETER InfoBasePath
|
||||
Путь к файловой информационной базе
|
||||
|
||||
.PARAMETER InfoBaseServer
|
||||
Сервер 1С (для серверной базы)
|
||||
|
||||
.PARAMETER InfoBaseRef
|
||||
Имя базы на сервере
|
||||
|
||||
.PARAMETER UserName
|
||||
Имя пользователя 1С
|
||||
|
||||
.PARAMETER Password
|
||||
Пароль пользователя
|
||||
|
||||
.PARAMETER InputFile
|
||||
Путь к DT-файлу для загрузки
|
||||
|
||||
.PARAMETER JobsCount
|
||||
Количество фоновых заданий для загрузки (0 = по числу процессоров)
|
||||
|
||||
.PARAMETER UnlockCode
|
||||
Код разблокировки базы (/UC) — если заблокировано начало сеансов
|
||||
|
||||
.EXAMPLE
|
||||
.\db-load-dt.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "backup.dt"
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$V8Path,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$InfoBasePath,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$InfoBaseServer,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$InfoBaseRef,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$UserName,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$Password,
|
||||
|
||||
[Parameter(Mandatory=$true)]
|
||||
[string]$InputFile,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[int]$JobsCount = 0,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$UnlockCode
|
||||
)
|
||||
|
||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# --- Resolve V8Path ---
|
||||
function Find-ProjectV8Path {
|
||||
$dir = (Get-Location).Path
|
||||
while ($dir) {
|
||||
$pf = Join-Path $dir ".v8-project.json"
|
||||
if (Test-Path $pf) {
|
||||
try {
|
||||
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||
if ($j.v8path) { return [string]$j.v8path }
|
||||
} catch {}
|
||||
return $null
|
||||
}
|
||||
$parent = Split-Path $dir -Parent
|
||||
if (-not $parent -or $parent -eq $dir) { break }
|
||||
$dir = $parent
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
if (-not $V8Path) {
|
||||
$V8Path = Find-ProjectV8Path
|
||||
}
|
||||
if (-not $V8Path) {
|
||||
$found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
|
||||
Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
|
||||
Select-Object -First 1
|
||||
if ($found) {
|
||||
$V8Path = $found.FullName
|
||||
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
||||
} else {
|
||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
if (Test-Path $V8Path -PathType Container) {
|
||||
$V8Path = Join-Path $V8Path "1cv8.exe"
|
||||
}
|
||||
|
||||
if (-not (Test-Path $V8Path)) {
|
||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Validate connection ---
|
||||
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
||||
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Validate input file ---
|
||||
if (-not (Test-Path $InputFile)) {
|
||||
Write-Host "Error: input file not found: $InputFile" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Temp dir ---
|
||||
$tempDir = Join-Path $env:TEMP "db_load_dt_$(Get-Random)"
|
||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
||||
|
||||
try {
|
||||
# --- Build arguments ---
|
||||
$arguments = @("DESIGNER")
|
||||
|
||||
if ($InfoBaseServer -and $InfoBaseRef) {
|
||||
$arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`""
|
||||
} else {
|
||||
$arguments += "/F", "`"$InfoBasePath`""
|
||||
}
|
||||
|
||||
if ($UserName) { $arguments += "/N`"$UserName`"" }
|
||||
if ($Password) { $arguments += "/P`"$Password`"" }
|
||||
if ($UnlockCode) { $arguments += "/UC`"$UnlockCode`"" }
|
||||
|
||||
$arguments += "/RestoreIB", "`"$InputFile`""
|
||||
if ($JobsCount -gt 0) { $arguments += "-JobsCount", "$JobsCount" }
|
||||
|
||||
# --- Output ---
|
||||
$outFile = Join-Path $tempDir "load_dt_log.txt"
|
||||
$arguments += "/Out", "`"$outFile`""
|
||||
$arguments += "/DisableStartupDialogs"
|
||||
|
||||
# --- Execute ---
|
||||
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
|
||||
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
||||
$exitCode = $process.ExitCode
|
||||
|
||||
# --- Result ---
|
||||
if ($exitCode -eq 0) {
|
||||
Write-Host "Information base restored successfully from: $InputFile" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "Error restoring information base (code: $exitCode)" -ForegroundColor Red
|
||||
}
|
||||
|
||||
if (Test-Path $outFile) {
|
||||
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
|
||||
if ($logContent) {
|
||||
Write-Host "--- Log ---"
|
||||
Write-Host $logContent
|
||||
Write-Host "--- End ---"
|
||||
}
|
||||
}
|
||||
|
||||
exit $exitCode
|
||||
|
||||
} finally {
|
||||
if (Test-Path $tempDir) {
|
||||
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
#!/usr/bin/env python3
|
||||
# db-load-dt v1.1 — Load 1C information base from DT file
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
|
||||
def _find_project_v8path():
|
||||
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
||||
d = os.getcwd()
|
||||
while True:
|
||||
pf = os.path.join(d, ".v8-project.json")
|
||||
if os.path.isfile(pf):
|
||||
try:
|
||||
with open(pf, encoding="utf-8-sig") as f:
|
||||
data = json.load(f)
|
||||
v = data.get("v8path")
|
||||
if v:
|
||||
return v
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
parent = os.path.dirname(d)
|
||||
if parent == d:
|
||||
return None
|
||||
d = parent
|
||||
|
||||
|
||||
def _version_key(p):
|
||||
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
|
||||
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
|
||||
return [int(x) for x in re.findall(r"\d+", ver)]
|
||||
|
||||
|
||||
def resolve_v8path(v8path):
|
||||
"""Resolve path to 1cv8.exe."""
|
||||
if not v8path:
|
||||
v8path = _find_project_v8path()
|
||||
if not v8path:
|
||||
candidates = (
|
||||
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
||||
)
|
||||
if candidates:
|
||||
v8path = max(candidates, key=_version_key)
|
||||
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
|
||||
print(f"Auto-selected platform {ver}: {v8path}")
|
||||
else:
|
||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
if os.path.isdir(v8path):
|
||||
v8path = os.path.join(v8path, "1cv8.exe")
|
||||
if not os.path.isfile(v8path):
|
||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
return v8path
|
||||
|
||||
|
||||
def main():
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
sys.stderr.reconfigure(encoding="utf-8")
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Load 1C information base from DT file",
|
||||
allow_abbrev=False,
|
||||
)
|
||||
parser.add_argument("-V8Path", default="")
|
||||
parser.add_argument("-InfoBasePath", default="")
|
||||
parser.add_argument("-InfoBaseServer", default="")
|
||||
parser.add_argument("-InfoBaseRef", default="")
|
||||
parser.add_argument("-UserName", default="")
|
||||
parser.add_argument("-Password", default="")
|
||||
parser.add_argument("-InputFile", required=True)
|
||||
parser.add_argument("-JobsCount", type=int, default=0)
|
||||
parser.add_argument("-UnlockCode", default="")
|
||||
args = parser.parse_args()
|
||||
|
||||
v8path = resolve_v8path(args.V8Path)
|
||||
|
||||
# --- Validate connection ---
|
||||
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# --- Validate input file ---
|
||||
if not os.path.isfile(args.InputFile):
|
||||
print(f"Error: input file not found: {args.InputFile}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# --- Temp dir ---
|
||||
temp_dir = os.path.join(tempfile.gettempdir(), f"db_load_dt_{random.randint(0, 999999)}")
|
||||
os.makedirs(temp_dir, exist_ok=True)
|
||||
|
||||
try:
|
||||
# --- Build arguments ---
|
||||
arguments = ["DESIGNER"]
|
||||
|
||||
if args.InfoBaseServer and args.InfoBaseRef:
|
||||
arguments.extend(["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"])
|
||||
else:
|
||||
arguments.extend(["/F", args.InfoBasePath])
|
||||
|
||||
if args.UserName:
|
||||
arguments.append(f"/N{args.UserName}")
|
||||
if args.Password:
|
||||
arguments.append(f"/P{args.Password}")
|
||||
if args.UnlockCode:
|
||||
arguments.append(f"/UC{args.UnlockCode}")
|
||||
|
||||
arguments.extend(["/RestoreIB", args.InputFile])
|
||||
if args.JobsCount > 0:
|
||||
arguments.extend(["-JobsCount", str(args.JobsCount)])
|
||||
|
||||
# --- Output ---
|
||||
out_file = os.path.join(temp_dir, "load_dt_log.txt")
|
||||
arguments.extend(["/Out", out_file])
|
||||
arguments.append("/DisableStartupDialogs")
|
||||
|
||||
# --- Execute ---
|
||||
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
||||
result = subprocess.run(
|
||||
[v8path] + arguments,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
exit_code = result.returncode
|
||||
|
||||
# --- Result ---
|
||||
if exit_code == 0:
|
||||
print(f"Information base restored successfully from: {args.InputFile}")
|
||||
else:
|
||||
print(f"Error restoring information base (code: {exit_code})", file=sys.stderr)
|
||||
|
||||
if os.path.isfile(out_file):
|
||||
try:
|
||||
with open(out_file, "r", encoding="utf-8-sig") as f:
|
||||
log_content = f.read()
|
||||
if log_content:
|
||||
print("--- Log ---")
|
||||
print(log_content)
|
||||
print("--- End ---")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
sys.exit(exit_code)
|
||||
|
||||
finally:
|
||||
if os.path.isdir(temp_dir):
|
||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -64,8 +64,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-git.ps1" <
|
||||
|
||||
## После выполнения
|
||||
|
||||
1. Показать список загруженных файлов и результат из лога
|
||||
2. Если `-UpdateDB` не был указан — **предложить `/db-update`** для применения изменений к БД
|
||||
Если `-UpdateDB` не был указан — **предложить `/db-update`** для применения изменений к БД
|
||||
|
||||
## Примеры
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# db-load-git v1.3 — Load Git changes into 1C database
|
||||
# db-load-git v1.5 — Load Git changes into 1C database
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
<#
|
||||
.SYNOPSIS
|
||||
@@ -120,15 +120,40 @@ function Get-ObjectXmlFromSubFile {
|
||||
|
||||
# --- Resolve V8Path (skip if DryRun) ---
|
||||
if (-not $DryRun) {
|
||||
function Find-ProjectV8Path {
|
||||
$dir = (Get-Location).Path
|
||||
while ($dir) {
|
||||
$pf = Join-Path $dir ".v8-project.json"
|
||||
if (Test-Path $pf) {
|
||||
try {
|
||||
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||
if ($j.v8path) { return [string]$j.v8path }
|
||||
} catch {}
|
||||
return $null
|
||||
}
|
||||
$parent = Split-Path $dir -Parent
|
||||
if (-not $parent -or $parent -eq $dir) { break }
|
||||
$dir = $parent
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
if (-not $V8Path) {
|
||||
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
|
||||
$V8Path = Find-ProjectV8Path
|
||||
}
|
||||
if (-not $V8Path) {
|
||||
$found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
|
||||
Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
|
||||
Select-Object -First 1
|
||||
if ($found) {
|
||||
$V8Path = $found.FullName
|
||||
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
||||
} else {
|
||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
} elseif (Test-Path $V8Path -PathType Container) {
|
||||
}
|
||||
if (Test-Path $V8Path -PathType Container) {
|
||||
$V8Path = Join-Path $V8Path "1cv8.exe"
|
||||
}
|
||||
|
||||
@@ -216,13 +241,16 @@ Write-Host "Git changes detected: $($changedFiles.Count) files"
|
||||
|
||||
# --- Filter and map to config files ---
|
||||
$configFiles = @()
|
||||
$supportSkipped = @()
|
||||
|
||||
foreach ($file in $changedFiles) {
|
||||
$file = $file.Trim().Replace('\', '/')
|
||||
if ([string]::IsNullOrWhiteSpace($file)) { continue }
|
||||
|
||||
# Skip service files
|
||||
if ($file -eq "ConfigDumpInfo.xml") { continue }
|
||||
# Skip service files (not partially loadable). Support-state files are tracked
|
||||
# to warn the user: support changes apply only via a full load.
|
||||
if ($file -match 'ParentConfigurations\.bin$') { $supportSkipped += $file; continue }
|
||||
if ($file -eq "ConfigDumpInfo.xml" -or $file -match '(^|/)ConfigDumpInfo\.xml$') { continue }
|
||||
|
||||
$fullPath = Join-Path $ConfigDir $file
|
||||
|
||||
@@ -265,6 +293,12 @@ foreach ($file in $changedFiles) {
|
||||
}
|
||||
}
|
||||
|
||||
if ($supportSkipped.Count -gt 0) {
|
||||
Write-Host "[ВНИМАНИЕ] Состояние поддержки изменено в коммите, но частично не загружается (исключено):" -ForegroundColor Yellow
|
||||
foreach ($sf in $supportSkipped) { Write-Host " - $sf" -ForegroundColor Yellow }
|
||||
Write-Host " Смена состояния поддержки применяется только полной загрузкой (db-load-xml -Mode Full)." -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
if ($configFiles.Count -eq 0) {
|
||||
Write-Host "No configuration files found in changes"
|
||||
exit 0
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
#!/usr/bin/env python3
|
||||
# db-load-git v1.3 — Load Git changes into 1C database
|
||||
# db-load-git v1.5 — Load Git changes into 1C database
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
@@ -13,23 +14,54 @@ import sys
|
||||
import tempfile
|
||||
|
||||
|
||||
def _find_project_v8path():
|
||||
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
||||
d = os.getcwd()
|
||||
while True:
|
||||
pf = os.path.join(d, ".v8-project.json")
|
||||
if os.path.isfile(pf):
|
||||
try:
|
||||
with open(pf, encoding="utf-8-sig") as f:
|
||||
data = json.load(f)
|
||||
v = data.get("v8path")
|
||||
if v:
|
||||
return v
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
parent = os.path.dirname(d)
|
||||
if parent == d:
|
||||
return None
|
||||
d = parent
|
||||
|
||||
|
||||
def _version_key(p):
|
||||
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
|
||||
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
|
||||
return [int(x) for x in re.findall(r"\d+", ver)]
|
||||
|
||||
|
||||
def resolve_v8path(v8path):
|
||||
"""Resolve path to 1cv8.exe."""
|
||||
if not v8path:
|
||||
candidates = glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||
v8path = _find_project_v8path()
|
||||
if not v8path:
|
||||
candidates = (
|
||||
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
||||
)
|
||||
if candidates:
|
||||
candidates.sort()
|
||||
return candidates[-1]
|
||||
v8path = max(candidates, key=_version_key)
|
||||
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
|
||||
print(f"Auto-selected platform {ver}: {v8path}")
|
||||
else:
|
||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
elif os.path.isdir(v8path):
|
||||
if os.path.isdir(v8path):
|
||||
v8path = os.path.join(v8path, "1cv8.exe")
|
||||
|
||||
if not os.path.isfile(v8path):
|
||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
return v8path
|
||||
|
||||
|
||||
@@ -146,14 +178,19 @@ def main():
|
||||
|
||||
# --- Filter and map to config files ---
|
||||
config_files = []
|
||||
support_skipped = []
|
||||
|
||||
for file in changed_files:
|
||||
file = file.strip().replace("\\", "/")
|
||||
if not file:
|
||||
continue
|
||||
|
||||
# Skip service files
|
||||
if file == "ConfigDumpInfo.xml":
|
||||
# Skip service files (not partially loadable). Support-state files are
|
||||
# tracked to warn: support changes apply only via a full load.
|
||||
if file.endswith("ParentConfigurations.bin"):
|
||||
support_skipped.append(file)
|
||||
continue
|
||||
if file == "ConfigDumpInfo.xml" or file.endswith("/ConfigDumpInfo.xml"):
|
||||
continue
|
||||
|
||||
full_path = os.path.join(args.ConfigDir, file)
|
||||
@@ -186,6 +223,12 @@ def main():
|
||||
if rel_path not in config_files:
|
||||
config_files.append(rel_path)
|
||||
|
||||
if support_skipped:
|
||||
print("[ВНИМАНИЕ] Состояние поддержки изменено в коммите, но частично не загружается (исключено):", file=sys.stderr)
|
||||
for sf in support_skipped:
|
||||
print(f" - {sf}", file=sys.stderr)
|
||||
print(" Смена состояния поддержки применяется только полной загрузкой (db-load-xml -Mode Full).", file=sys.stderr)
|
||||
|
||||
if len(config_files) == 0:
|
||||
print("No configuration files found in changes")
|
||||
sys.exit(0)
|
||||
|
||||
@@ -80,17 +80,9 @@ Documents/Заказ.xml
|
||||
Documents/Заказ/Forms/ФормаДокумента.xml
|
||||
```
|
||||
|
||||
## Коды возврата
|
||||
|
||||
| Код | Описание |
|
||||
|-----|----------|
|
||||
| 0 | Успешно |
|
||||
| 1 | Ошибка (см. лог) |
|
||||
|
||||
## После выполнения
|
||||
|
||||
1. Прочитай лог и покажи результат
|
||||
2. Если `-UpdateDB` не был указан — **предложи выполнить `/db-update`** для применения изменений к БД
|
||||
Если `-UpdateDB` не был указан — **предложи выполнить `/db-update`** для применения изменений к БД
|
||||
|
||||
## Примеры
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# db-load-xml v1.3 — Load 1C configuration from XML files
|
||||
# db-load-xml v1.5 — Load 1C configuration from XML files
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
<#
|
||||
.SYNOPSIS
|
||||
@@ -108,15 +108,40 @@ $OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# --- Resolve V8Path ---
|
||||
function Find-ProjectV8Path {
|
||||
$dir = (Get-Location).Path
|
||||
while ($dir) {
|
||||
$pf = Join-Path $dir ".v8-project.json"
|
||||
if (Test-Path $pf) {
|
||||
try {
|
||||
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||
if ($j.v8path) { return [string]$j.v8path }
|
||||
} catch {}
|
||||
return $null
|
||||
}
|
||||
$parent = Split-Path $dir -Parent
|
||||
if (-not $parent -or $parent -eq $dir) { break }
|
||||
$dir = $parent
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
if (-not $V8Path) {
|
||||
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
|
||||
$V8Path = Find-ProjectV8Path
|
||||
}
|
||||
if (-not $V8Path) {
|
||||
$found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
|
||||
Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
|
||||
Select-Object -First 1
|
||||
if ($found) {
|
||||
$V8Path = $found.FullName
|
||||
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
||||
} else {
|
||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
} elseif (Test-Path $V8Path -PathType Container) {
|
||||
}
|
||||
if (Test-Path $V8Path -PathType Container) {
|
||||
$V8Path = Join-Path $V8Path "1cv8.exe"
|
||||
}
|
||||
|
||||
@@ -168,25 +193,36 @@ try {
|
||||
Write-Host "Executing partial configuration load..."
|
||||
|
||||
# Build list file
|
||||
$generatedListFile = $null
|
||||
$rawList = @()
|
||||
if ($ListFile) {
|
||||
# Use provided list file
|
||||
if (-not (Test-Path $ListFile)) {
|
||||
Write-Host "Error: list file not found: $ListFile" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
$generatedListFile = $ListFile
|
||||
$rawList = @(Get-Content -Path $ListFile -Encoding UTF8 | ForEach-Object { $_.Trim() } | Where-Object { $_ })
|
||||
} else {
|
||||
# Generate from -Files parameter
|
||||
$fileList = $Files -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ }
|
||||
$generatedListFile = Join-Path $tempDir "load_list.txt"
|
||||
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
||||
[System.IO.File]::WriteAllLines($generatedListFile, $fileList, $utf8Bom)
|
||||
|
||||
Write-Host "Files to load: $($fileList.Count)"
|
||||
foreach ($f in $fileList) { Write-Host " $f" }
|
||||
$rawList = @($Files -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ })
|
||||
}
|
||||
|
||||
# Support-state service files are NOT partially loadable — exclude with a hint.
|
||||
$supportRe = 'ParentConfigurations\.bin$|(^|[\\/])ConfigDumpInfo\.xml$'
|
||||
$supportFiles = @($rawList | Where-Object { $_ -match $supportRe })
|
||||
$fileList = @($rawList | Where-Object { $_ -notmatch $supportRe })
|
||||
if ($supportFiles.Count -gt 0) {
|
||||
Write-Host "[ВНИМАНИЕ] Служебные файлы состояния поддержки исключены из частичной загрузки (частично не грузятся):" -ForegroundColor Yellow
|
||||
foreach ($sf in $supportFiles) { Write-Host " - $sf" -ForegroundColor Yellow }
|
||||
Write-Host " Смена состояния поддержки применяется только полной загрузкой: -Mode Full." -ForegroundColor Yellow
|
||||
}
|
||||
if ($fileList.Count -eq 0) {
|
||||
Write-Host "Error: после исключения служебных файлов поддержки загружать нечего. Для смены поддержки используйте -Mode Full." -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
$generatedListFile = Join-Path $tempDir "load_list.txt"
|
||||
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
||||
[System.IO.File]::WriteAllLines($generatedListFile, $fileList, $utf8Bom)
|
||||
Write-Host "Files to load: $($fileList.Count)"
|
||||
foreach ($f in $fileList) { Write-Host " $f" }
|
||||
|
||||
$arguments += "-listFile", "`"$generatedListFile`""
|
||||
$arguments += "-partial"
|
||||
$arguments += "-updateConfigDumpInfo"
|
||||
|
||||
@@ -1,34 +1,67 @@
|
||||
#!/usr/bin/env python3
|
||||
# db-load-xml v1.3 — Load 1C configuration from XML files
|
||||
# db-load-xml v1.5 — Load 1C configuration from XML files
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
|
||||
def _find_project_v8path():
|
||||
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
||||
d = os.getcwd()
|
||||
while True:
|
||||
pf = os.path.join(d, ".v8-project.json")
|
||||
if os.path.isfile(pf):
|
||||
try:
|
||||
with open(pf, encoding="utf-8-sig") as f:
|
||||
data = json.load(f)
|
||||
v = data.get("v8path")
|
||||
if v:
|
||||
return v
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
parent = os.path.dirname(d)
|
||||
if parent == d:
|
||||
return None
|
||||
d = parent
|
||||
|
||||
|
||||
def _version_key(p):
|
||||
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
|
||||
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
|
||||
return [int(x) for x in re.findall(r"\d+", ver)]
|
||||
|
||||
|
||||
def resolve_v8path(v8path):
|
||||
"""Resolve path to 1cv8.exe."""
|
||||
if not v8path:
|
||||
candidates = glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||
v8path = _find_project_v8path()
|
||||
if not v8path:
|
||||
candidates = (
|
||||
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
||||
)
|
||||
if candidates:
|
||||
candidates.sort()
|
||||
return candidates[-1]
|
||||
v8path = max(candidates, key=_version_key)
|
||||
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
|
||||
print(f"Auto-selected platform {ver}: {v8path}")
|
||||
else:
|
||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
elif os.path.isdir(v8path):
|
||||
if os.path.isdir(v8path):
|
||||
v8path = os.path.join(v8path, "1cv8.exe")
|
||||
|
||||
if not os.path.isfile(v8path):
|
||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
return v8path
|
||||
|
||||
|
||||
@@ -114,23 +147,33 @@ def main():
|
||||
print("Executing partial configuration load...")
|
||||
|
||||
# Build list file
|
||||
generated_list_file = None
|
||||
if args.ListFile:
|
||||
# Use provided list file
|
||||
if not os.path.isfile(args.ListFile):
|
||||
print(f"Error: list file not found: {args.ListFile}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
generated_list_file = args.ListFile
|
||||
with open(args.ListFile, encoding="utf-8-sig") as f:
|
||||
raw_list = [ln.strip() for ln in f if ln.strip()]
|
||||
else:
|
||||
# Generate from -Files parameter
|
||||
file_list = [f.strip() for f in args.Files.split(",") if f.strip()]
|
||||
generated_list_file = os.path.join(temp_dir, "load_list.txt")
|
||||
with open(generated_list_file, "w", encoding="utf-8-sig") as f:
|
||||
f.write("\n".join(file_list))
|
||||
raw_list = [f.strip() for f in args.Files.split(",") if f.strip()]
|
||||
|
||||
print(f"Files to load: {len(file_list)}")
|
||||
for fl in file_list:
|
||||
print(f" {fl}")
|
||||
# Support-state service files are NOT partially loadable — exclude with a hint.
|
||||
support_re = re.compile(r"ParentConfigurations\.bin$|(^|[\\/])ConfigDumpInfo\.xml$")
|
||||
support_files = [x for x in raw_list if support_re.search(x)]
|
||||
file_list = [x for x in raw_list if not support_re.search(x)]
|
||||
if support_files:
|
||||
print("[ВНИМАНИЕ] Служебные файлы состояния поддержки исключены из частичной загрузки (частично не грузятся):", file=sys.stderr)
|
||||
for sf in support_files:
|
||||
print(f" - {sf}", file=sys.stderr)
|
||||
print(" Смена состояния поддержки применяется только полной загрузкой: -Mode Full.", file=sys.stderr)
|
||||
if not file_list:
|
||||
print("Error: после исключения служебных файлов поддержки загружать нечего. Для смены поддержки используйте -Mode Full.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
generated_list_file = os.path.join(temp_dir, "load_list.txt")
|
||||
with open(generated_list_file, "w", encoding="utf-8-sig") as f:
|
||||
f.write("\n".join(file_list))
|
||||
print(f"Files to load: {len(file_list)}")
|
||||
for fl in file_list:
|
||||
print(f" {fl}")
|
||||
|
||||
arguments += ["-listFile", generated_list_file]
|
||||
arguments.append("-partial")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# db-run v1.0 — Launch 1C:Enterprise
|
||||
# db-run v1.1 — Launch 1C:Enterprise
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
<#
|
||||
.SYNOPSIS
|
||||
@@ -79,15 +79,40 @@ $OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# --- Resolve V8Path ---
|
||||
function Find-ProjectV8Path {
|
||||
$dir = (Get-Location).Path
|
||||
while ($dir) {
|
||||
$pf = Join-Path $dir ".v8-project.json"
|
||||
if (Test-Path $pf) {
|
||||
try {
|
||||
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||
if ($j.v8path) { return [string]$j.v8path }
|
||||
} catch {}
|
||||
return $null
|
||||
}
|
||||
$parent = Split-Path $dir -Parent
|
||||
if (-not $parent -or $parent -eq $dir) { break }
|
||||
$dir = $parent
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
if (-not $V8Path) {
|
||||
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
|
||||
$V8Path = Find-ProjectV8Path
|
||||
}
|
||||
if (-not $V8Path) {
|
||||
$found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
|
||||
Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
|
||||
Select-Object -First 1
|
||||
if ($found) {
|
||||
$V8Path = $found.FullName
|
||||
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
||||
} else {
|
||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
} elseif (Test-Path $V8Path -PathType Container) {
|
||||
}
|
||||
if (Test-Path $V8Path -PathType Container) {
|
||||
$V8Path = Join-Path $V8Path "1cv8.exe"
|
||||
}
|
||||
|
||||
|
||||
@@ -1,26 +1,61 @@
|
||||
#!/usr/bin/env python3
|
||||
# db-run v1.0 — Launch 1C:Enterprise
|
||||
# db-run v1.1 — Launch 1C:Enterprise
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
def _find_project_v8path():
|
||||
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
||||
d = os.getcwd()
|
||||
while True:
|
||||
pf = os.path.join(d, ".v8-project.json")
|
||||
if os.path.isfile(pf):
|
||||
try:
|
||||
with open(pf, encoding="utf-8-sig") as f:
|
||||
data = json.load(f)
|
||||
v = data.get("v8path")
|
||||
if v:
|
||||
return v
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
parent = os.path.dirname(d)
|
||||
if parent == d:
|
||||
return None
|
||||
d = parent
|
||||
|
||||
|
||||
def _version_key(p):
|
||||
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
|
||||
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
|
||||
return [int(x) for x in re.findall(r"\d+", ver)]
|
||||
|
||||
|
||||
def resolve_v8path(v8path):
|
||||
"""Resolve path to 1cv8.exe."""
|
||||
if not v8path:
|
||||
found = sorted(glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe"))
|
||||
if found:
|
||||
return found[-1]
|
||||
v8path = _find_project_v8path()
|
||||
if not v8path:
|
||||
candidates = (
|
||||
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
||||
)
|
||||
if candidates:
|
||||
v8path = max(candidates, key=_version_key)
|
||||
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
|
||||
print(f"Auto-selected platform {ver}: {v8path}")
|
||||
else:
|
||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
elif os.path.isdir(v8path):
|
||||
if os.path.isdir(v8path):
|
||||
v8path = os.path.join(v8path, "1cv8.exe")
|
||||
|
||||
if not os.path.isfile(v8path):
|
||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
@@ -66,13 +66,6 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" <п
|
||||
| `-BackgroundSuspend` | Приостановить |
|
||||
| `-BackgroundResume` | Возобновить |
|
||||
|
||||
## Коды возврата
|
||||
|
||||
| Код | Описание |
|
||||
|-----|----------|
|
||||
| 0 | Успешно |
|
||||
| 1 | Ошибка (см. лог) |
|
||||
|
||||
## Предупреждения
|
||||
|
||||
- Если обновление **не динамическое** — потребуется **монопольный доступ** к базе (все пользователи должны выйти)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# db-update v1.0 — Update 1C database configuration
|
||||
# db-update v1.1 — Update 1C database configuration
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
<#
|
||||
.SYNOPSIS
|
||||
@@ -89,15 +89,40 @@ $OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# --- Resolve V8Path ---
|
||||
function Find-ProjectV8Path {
|
||||
$dir = (Get-Location).Path
|
||||
while ($dir) {
|
||||
$pf = Join-Path $dir ".v8-project.json"
|
||||
if (Test-Path $pf) {
|
||||
try {
|
||||
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||
if ($j.v8path) { return [string]$j.v8path }
|
||||
} catch {}
|
||||
return $null
|
||||
}
|
||||
$parent = Split-Path $dir -Parent
|
||||
if (-not $parent -or $parent -eq $dir) { break }
|
||||
$dir = $parent
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
if (-not $V8Path) {
|
||||
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
|
||||
$V8Path = Find-ProjectV8Path
|
||||
}
|
||||
if (-not $V8Path) {
|
||||
$found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
|
||||
Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
|
||||
Select-Object -First 1
|
||||
if ($found) {
|
||||
$V8Path = $found.FullName
|
||||
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
||||
} else {
|
||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
} elseif (Test-Path $V8Path -PathType Container) {
|
||||
}
|
||||
if (Test-Path $V8Path -PathType Container) {
|
||||
$V8Path = Join-Path $V8Path "1cv8.exe"
|
||||
}
|
||||
|
||||
|
||||
@@ -1,29 +1,64 @@
|
||||
#!/usr/bin/env python3
|
||||
# db-update v1.0 — Update 1C database configuration
|
||||
# db-update v1.1 — Update 1C database configuration
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
|
||||
def _find_project_v8path():
|
||||
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
||||
d = os.getcwd()
|
||||
while True:
|
||||
pf = os.path.join(d, ".v8-project.json")
|
||||
if os.path.isfile(pf):
|
||||
try:
|
||||
with open(pf, encoding="utf-8-sig") as f:
|
||||
data = json.load(f)
|
||||
v = data.get("v8path")
|
||||
if v:
|
||||
return v
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
parent = os.path.dirname(d)
|
||||
if parent == d:
|
||||
return None
|
||||
d = parent
|
||||
|
||||
|
||||
def _version_key(p):
|
||||
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
|
||||
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
|
||||
return [int(x) for x in re.findall(r"\d+", ver)]
|
||||
|
||||
|
||||
def resolve_v8path(v8path):
|
||||
"""Resolve path to 1cv8.exe."""
|
||||
if not v8path:
|
||||
found = sorted(glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe"))
|
||||
if found:
|
||||
return found[-1]
|
||||
v8path = _find_project_v8path()
|
||||
if not v8path:
|
||||
candidates = (
|
||||
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
||||
)
|
||||
if candidates:
|
||||
v8path = max(candidates, key=_version_key)
|
||||
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
|
||||
print(f"Auto-selected platform {ver}: {v8path}")
|
||||
else:
|
||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
elif os.path.isdir(v8path):
|
||||
if os.path.isdir(v8path):
|
||||
v8path = os.path.join(v8path, "1cv8.exe")
|
||||
|
||||
if not os.path.isfile(v8path):
|
||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# epf-build v1.0 — Build external data processor or report (EPF/ERF) from XML sources
|
||||
# epf-build v1.1 — Build external data processor or report (EPF/ERF) from XML sources
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
<#
|
||||
.SYNOPSIS
|
||||
@@ -70,15 +70,40 @@ $OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# --- Resolve V8Path ---
|
||||
function Find-ProjectV8Path {
|
||||
$dir = (Get-Location).Path
|
||||
while ($dir) {
|
||||
$pf = Join-Path $dir ".v8-project.json"
|
||||
if (Test-Path $pf) {
|
||||
try {
|
||||
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||
if ($j.v8path) { return [string]$j.v8path }
|
||||
} catch {}
|
||||
return $null
|
||||
}
|
||||
$parent = Split-Path $dir -Parent
|
||||
if (-not $parent -or $parent -eq $dir) { break }
|
||||
$dir = $parent
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
if (-not $V8Path) {
|
||||
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
|
||||
$V8Path = Find-ProjectV8Path
|
||||
}
|
||||
if (-not $V8Path) {
|
||||
$found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
|
||||
Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
|
||||
Select-Object -First 1
|
||||
if ($found) {
|
||||
$V8Path = $found.FullName
|
||||
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
||||
} else {
|
||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
} elseif (Test-Path $V8Path -PathType Container) {
|
||||
}
|
||||
if (Test-Path $V8Path -PathType Container) {
|
||||
$V8Path = Join-Path $V8Path "1cv8.exe"
|
||||
}
|
||||
|
||||
|
||||
@@ -1,34 +1,67 @@
|
||||
#!/usr/bin/env python3
|
||||
# epf-build v1.0 — Build external data processor or report (EPF/ERF) from XML sources
|
||||
# epf-build v1.1 — Build external data processor or report (EPF/ERF) from XML sources
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
|
||||
def _find_project_v8path():
|
||||
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
||||
d = os.getcwd()
|
||||
while True:
|
||||
pf = os.path.join(d, ".v8-project.json")
|
||||
if os.path.isfile(pf):
|
||||
try:
|
||||
with open(pf, encoding="utf-8-sig") as f:
|
||||
data = json.load(f)
|
||||
v = data.get("v8path")
|
||||
if v:
|
||||
return v
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
parent = os.path.dirname(d)
|
||||
if parent == d:
|
||||
return None
|
||||
d = parent
|
||||
|
||||
|
||||
def _version_key(p):
|
||||
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
|
||||
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
|
||||
return [int(x) for x in re.findall(r"\d+", ver)]
|
||||
|
||||
|
||||
def resolve_v8path(v8path):
|
||||
"""Resolve path to 1cv8.exe."""
|
||||
if not v8path:
|
||||
candidates = glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||
v8path = _find_project_v8path()
|
||||
if not v8path:
|
||||
candidates = (
|
||||
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
||||
)
|
||||
if candidates:
|
||||
candidates.sort()
|
||||
return candidates[-1]
|
||||
v8path = max(candidates, key=_version_key)
|
||||
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
|
||||
print(f"Auto-selected platform {ver}: {v8path}")
|
||||
else:
|
||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
elif os.path.isdir(v8path):
|
||||
if os.path.isdir(v8path):
|
||||
v8path = os.path.join(v8path, "1cv8.exe")
|
||||
|
||||
if not os.path.isfile(v8path):
|
||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
return v8path
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# epf-dump v1.0 — Dump external data processor or report (EPF/ERF) to XML sources
|
||||
# epf-dump v1.1 — Dump external data processor or report (EPF/ERF) to XML sources
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
<#
|
||||
.SYNOPSIS
|
||||
@@ -77,15 +77,40 @@ $OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# --- Resolve V8Path ---
|
||||
function Find-ProjectV8Path {
|
||||
$dir = (Get-Location).Path
|
||||
while ($dir) {
|
||||
$pf = Join-Path $dir ".v8-project.json"
|
||||
if (Test-Path $pf) {
|
||||
try {
|
||||
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||
if ($j.v8path) { return [string]$j.v8path }
|
||||
} catch {}
|
||||
return $null
|
||||
}
|
||||
$parent = Split-Path $dir -Parent
|
||||
if (-not $parent -or $parent -eq $dir) { break }
|
||||
$dir = $parent
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
if (-not $V8Path) {
|
||||
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
|
||||
$V8Path = Find-ProjectV8Path
|
||||
}
|
||||
if (-not $V8Path) {
|
||||
$found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
|
||||
Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
|
||||
Select-Object -First 1
|
||||
if ($found) {
|
||||
$V8Path = $found.FullName
|
||||
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
||||
} else {
|
||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
} elseif (Test-Path $V8Path -PathType Container) {
|
||||
}
|
||||
if (Test-Path $V8Path -PathType Container) {
|
||||
$V8Path = Join-Path $V8Path "1cv8.exe"
|
||||
}
|
||||
|
||||
|
||||
@@ -1,34 +1,67 @@
|
||||
#!/usr/bin/env python3
|
||||
# epf-dump v1.0 — Dump external data processor or report (EPF/ERF) to XML sources
|
||||
# epf-dump v1.1 — Dump external data processor or report (EPF/ERF) to XML sources
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
|
||||
def _find_project_v8path():
|
||||
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
||||
d = os.getcwd()
|
||||
while True:
|
||||
pf = os.path.join(d, ".v8-project.json")
|
||||
if os.path.isfile(pf):
|
||||
try:
|
||||
with open(pf, encoding="utf-8-sig") as f:
|
||||
data = json.load(f)
|
||||
v = data.get("v8path")
|
||||
if v:
|
||||
return v
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
parent = os.path.dirname(d)
|
||||
if parent == d:
|
||||
return None
|
||||
d = parent
|
||||
|
||||
|
||||
def _version_key(p):
|
||||
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
|
||||
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
|
||||
return [int(x) for x in re.findall(r"\d+", ver)]
|
||||
|
||||
|
||||
def resolve_v8path(v8path):
|
||||
"""Resolve path to 1cv8.exe."""
|
||||
if not v8path:
|
||||
candidates = glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||
v8path = _find_project_v8path()
|
||||
if not v8path:
|
||||
candidates = (
|
||||
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
||||
)
|
||||
if candidates:
|
||||
candidates.sort()
|
||||
return candidates[-1]
|
||||
v8path = max(candidates, key=_version_key)
|
||||
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
|
||||
print(f"Auto-selected platform {ver}: {v8path}")
|
||||
else:
|
||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
elif os.path.isdir(v8path):
|
||||
if os.path.isdir(v8path):
|
||||
v8path = os.path.join(v8path, "1cv8.exe")
|
||||
|
||||
if not os.path.isfile(v8path):
|
||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
return v8path
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# form-add v1.5 — Add managed form to 1C config object
|
||||
# form-add v1.7 — Add managed form to 1C config object
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
@@ -18,6 +18,124 @@ $ErrorActionPreference = "Stop"
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
[Console]::InputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# --- Support guard (Ext/ParentConfigurations.bin) ---
|
||||
# See docs/1c-support-state-spec.md. Blocks edits of vendor objects "на замке" /
|
||||
# read-only configs unless allowed. Trigger = bin present; reaction from
|
||||
# .v8-project.json editingAllowedCheck (deny|warn|off, default deny). Never
|
||||
# throws — guard errors degrade to allow.
|
||||
function Get-RootUuid([string]$xmlPath) {
|
||||
if (-not (Test-Path $xmlPath)) { return $null }
|
||||
try {
|
||||
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
|
||||
$el = $mx.DocumentElement.FirstChild
|
||||
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
|
||||
if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } }
|
||||
} catch {}
|
||||
return $null
|
||||
}
|
||||
function Find-V8Project([string]$startDir) {
|
||||
$d = $startDir
|
||||
for ($i = 0; $i -lt 20 -and $d; $i++) {
|
||||
$pj = Join-Path $d ".v8-project.json"
|
||||
if (Test-Path $pj) { return $pj }
|
||||
$parent = [System.IO.Path]::GetDirectoryName($d)
|
||||
if ($parent -eq $d) { break }
|
||||
$d = $parent
|
||||
}
|
||||
return $null
|
||||
}
|
||||
function Get-EditMode([string]$cfgDir) {
|
||||
try {
|
||||
$pj = Find-V8Project (Get-Location).Path
|
||||
if (-not $pj) { $pj = Find-V8Project $cfgDir }
|
||||
if (-not $pj) { return 'deny' }
|
||||
$proj = Get-Content -Raw $pj | ConvertFrom-Json
|
||||
$cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/')
|
||||
if ($proj.databases) {
|
||||
foreach ($db in $proj.databases) {
|
||||
if ($db.configSrc) {
|
||||
$src = [System.IO.Path]::GetFullPath($db.configSrc).TrimEnd('\', '/')
|
||||
if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) {
|
||||
if ($db.editingAllowedCheck) { return $db.editingAllowedCheck }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($proj.editingAllowedCheck) { return $proj.editingAllowedCheck }
|
||||
return 'deny'
|
||||
} catch { return 'deny' }
|
||||
}
|
||||
function Assert-EditAllowed([string]$targetPath, [string]$require) {
|
||||
try {
|
||||
$rp = $targetPath
|
||||
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
|
||||
$elemUuid = Get-RootUuid $rp
|
||||
$cfgDir = $null; $binPath = $null
|
||||
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
|
||||
for ($i = 0; $i -lt 12 -and $d; $i++) {
|
||||
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
|
||||
if (-not $cfgDir) {
|
||||
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
|
||||
if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $cfgDir = $d; $binPath = $cand }
|
||||
}
|
||||
if ($elemUuid -and $cfgDir) { break }
|
||||
$parent = [System.IO.Path]::GetDirectoryName($d)
|
||||
if ($parent -eq $d) { break }
|
||||
$d = $parent
|
||||
}
|
||||
# New object (no element file): fall back to config root uuid.
|
||||
if (-not $elemUuid -and $cfgDir) { $elemUuid = Get-RootUuid (Join-Path $cfgDir "Configuration.xml") }
|
||||
if (-not $binPath -or -not (Test-Path $binPath)) { return }
|
||||
$bytes = [System.IO.File]::ReadAllBytes($binPath)
|
||||
if ($bytes.Length -le 32) { return }
|
||||
$start = 0
|
||||
if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 }
|
||||
$text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start)
|
||||
$hm = [regex]::Match($text, '^\{6,(\d+),(\d+),')
|
||||
if (-not $hm.Success) { return }
|
||||
$G = [int]$hm.Groups[1].Value
|
||||
$K = [int]$hm.Groups[2].Value
|
||||
if ($K -eq 0) { return }
|
||||
$best = $null
|
||||
if ($elemUuid) {
|
||||
$u = [regex]::Escape($elemUuid.ToLower())
|
||||
foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) {
|
||||
$f1 = [int]$m.Groups[1].Value
|
||||
if ($null -eq $best -or $f1 -lt $best) { $best = $f1 }
|
||||
}
|
||||
}
|
||||
$blocked = $false; $code = ""; $reason = ""
|
||||
if ($G -eq 1) { $blocked = $true; $code = "capability-off"; $reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)" }
|
||||
elseif ($require -eq 'removed') {
|
||||
if ($null -ne $best -and $best -ne 2) { $blocked = $true; $code = "not-removed"; $reason = "объект не снят с поддержки — удаление сломает обновления" }
|
||||
}
|
||||
else {
|
||||
if ($null -ne $best -and $best -eq 0) { $blocked = $true; $code = "locked"; $reason = "объект на замке — редактирование сломает обновления" }
|
||||
}
|
||||
if (-not $blocked) { return }
|
||||
$mode = Get-EditMode $cfgDir
|
||||
if ($mode -eq 'off') { return }
|
||||
# Use Console.Error (not Write-Error) — under ErrorActionPreference=Stop the
|
||||
# latter throws and would be swallowed by this function's own catch.
|
||||
if ($mode -eq 'warn') { [Console]::Error.WriteLine("[support-guard] ПРЕДУПРЕЖДЕНИЕ: $reason. Цель: $rp"); return }
|
||||
$head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления."
|
||||
$cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются."
|
||||
$offNote = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json."
|
||||
if ($code -eq "capability-off") {
|
||||
$state = "Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «$rp» редактировать нельзя."
|
||||
$fix = "Либо снять защиту явно (навык support-edit, два шага):`n 1. support-edit -Path ""$cfgDir"" -Capability on — включить возможность изменения (объекты пока остаются на замке);`n 2. support-edit -Path ""$rp"" -Set editable — открыть этот объект для редактирования.`n Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора."
|
||||
} elseif ($code -eq "not-removed") {
|
||||
$state = "Состояние: объект «$rp» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора."
|
||||
$fix = "Либо сначала снять объект с поддержки, затем удалять:`n support-edit -Path ""$rp"" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно."
|
||||
} else {
|
||||
$state = "Состояние: объект «$rp» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)."
|
||||
$fix = "Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):`n support-edit -Path ""$rp"" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);`n support-edit -Path ""$rp"" -Set off-support — снять с поддержки: обновления по объекту больше не приходят."
|
||||
}
|
||||
[Console]::Error.WriteLine("$head`n$state`n$cfe`n$fix`n$offNote")
|
||||
exit 1
|
||||
} catch { return }
|
||||
}
|
||||
|
||||
# --- Detect XML format version ---
|
||||
|
||||
function Detect-FormatVersion([string]$dir) {
|
||||
@@ -55,6 +173,7 @@ if (-not (Test-Path $ObjectPath)) {
|
||||
}
|
||||
|
||||
$objectXmlFull = Resolve-Path $ObjectPath
|
||||
Assert-EditAllowed $objectXmlFull.Path 'editable'
|
||||
$script:formatVersion = Detect-FormatVersion (Split-Path $objectXmlFull.Path -Parent)
|
||||
|
||||
$xmlDoc = New-Object System.Xml.XmlDocument
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
#!/usr/bin/env python3
|
||||
# form-add v1.5 — Add managed form to 1C config object
|
||||
# form-add v1.7 — Add managed form to 1C config object
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
@@ -10,6 +11,166 @@ import uuid
|
||||
|
||||
from lxml import etree
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md
|
||||
# Blocks edits of vendor objects "на замке" / read-only configs. Trigger = bin
|
||||
# present; reaction from .v8-project.json editingAllowedCheck (deny|warn|off,
|
||||
# default deny). Never throws (except sys.exit on deny) — errors degrade to allow.
|
||||
# ============================================================
|
||||
|
||||
def _sg_root_uuid(xml_path):
|
||||
if not os.path.isfile(xml_path):
|
||||
return None
|
||||
try:
|
||||
mx = etree.parse(xml_path).getroot()
|
||||
for child in mx:
|
||||
if isinstance(child.tag, str) and child.get("uuid"):
|
||||
return child.get("uuid")
|
||||
except Exception:
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def _sg_find_v8project(start_dir):
|
||||
d = start_dir
|
||||
for _ in range(20):
|
||||
if not d:
|
||||
break
|
||||
pj = os.path.join(d, ".v8-project.json")
|
||||
if os.path.isfile(pj):
|
||||
return pj
|
||||
parent = os.path.dirname(d)
|
||||
if parent == d:
|
||||
break
|
||||
d = parent
|
||||
return None
|
||||
|
||||
|
||||
def _sg_get_edit_mode(cfg_dir):
|
||||
try:
|
||||
pj = _sg_find_v8project(os.getcwd()) or _sg_find_v8project(cfg_dir)
|
||||
if not pj:
|
||||
return "deny"
|
||||
proj = json.loads(open(pj, encoding="utf-8-sig").read())
|
||||
cfg_full = os.path.normcase(os.path.abspath(cfg_dir)).rstrip("\\/")
|
||||
for db in proj.get("databases", []):
|
||||
src = db.get("configSrc")
|
||||
if src:
|
||||
src_full = os.path.normcase(os.path.abspath(src)).rstrip("\\/")
|
||||
if cfg_full == src_full or cfg_full.startswith(src_full + os.sep):
|
||||
if db.get("editingAllowedCheck"):
|
||||
return db["editingAllowedCheck"]
|
||||
if proj.get("editingAllowedCheck"):
|
||||
return proj["editingAllowedCheck"]
|
||||
return "deny"
|
||||
except Exception:
|
||||
return "deny"
|
||||
|
||||
|
||||
def assert_edit_allowed(target_path, require):
|
||||
try:
|
||||
rp = os.path.abspath(target_path)
|
||||
elem_uuid = _sg_root_uuid(rp)
|
||||
cfg_dir = None
|
||||
bin_path = None
|
||||
d = rp if os.path.isdir(rp) else os.path.dirname(rp)
|
||||
for _ in range(12):
|
||||
if not d:
|
||||
break
|
||||
if not elem_uuid:
|
||||
elem_uuid = _sg_root_uuid(d + ".xml")
|
||||
if not cfg_dir:
|
||||
cand = os.path.join(d, "Ext", "ParentConfigurations.bin")
|
||||
if os.path.exists(cand) or os.path.exists(os.path.join(d, "Configuration.xml")):
|
||||
cfg_dir = d
|
||||
bin_path = cand
|
||||
if elem_uuid and cfg_dir:
|
||||
break
|
||||
parent = os.path.dirname(d)
|
||||
if parent == d:
|
||||
break
|
||||
d = parent
|
||||
if not elem_uuid and cfg_dir:
|
||||
elem_uuid = _sg_root_uuid(os.path.join(cfg_dir, "Configuration.xml"))
|
||||
if not bin_path or not os.path.exists(bin_path):
|
||||
return
|
||||
data = open(bin_path, "rb").read()
|
||||
if len(data) <= 32:
|
||||
return
|
||||
if data[:3] == b"\xef\xbb\xbf":
|
||||
data = data[3:]
|
||||
text = data.decode("utf-8", "replace")
|
||||
h = re.match(r"\{6,(\d+),(\d+),", text)
|
||||
if not h:
|
||||
return
|
||||
g = int(h.group(1))
|
||||
k = int(h.group(2))
|
||||
if k == 0:
|
||||
return
|
||||
best = None
|
||||
if elem_uuid:
|
||||
for m in re.finditer(r"([0-2]),0," + re.escape(elem_uuid.lower()), text):
|
||||
f1 = int(m.group(1))
|
||||
if best is None or f1 < best:
|
||||
best = f1
|
||||
blocked = False
|
||||
code = ""
|
||||
reason = ""
|
||||
if g == 1:
|
||||
blocked = True
|
||||
code = "capability-off"
|
||||
reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)"
|
||||
elif require == "removed":
|
||||
if best is not None and best != 2:
|
||||
blocked = True
|
||||
code = "not-removed"
|
||||
reason = "объект не снят с поддержки — удаление сломает обновления"
|
||||
else:
|
||||
if best is not None and best == 0:
|
||||
blocked = True
|
||||
code = "locked"
|
||||
reason = "объект на замке — редактирование сломает обновления"
|
||||
if not blocked:
|
||||
return
|
||||
mode = _sg_get_edit_mode(cfg_dir)
|
||||
if mode == "off":
|
||||
return
|
||||
if mode == "warn":
|
||||
sys.stderr.write(f"[support-guard] ПРЕДУПРЕЖДЕНИЕ: {reason}. Цель: {rp}\n")
|
||||
return
|
||||
head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления."
|
||||
cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются."
|
||||
off_note = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json."
|
||||
if code == "capability-off":
|
||||
state = f"Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «{rp}» редактировать нельзя."
|
||||
fix = (
|
||||
"Либо снять защиту явно (навык support-edit, два шага):\n"
|
||||
f' 1. support-edit -Path "{cfg_dir}" -Capability on — включить возможность изменения (объекты пока остаются на замке);\n'
|
||||
f' 2. support-edit -Path "{rp}" -Set editable — открыть этот объект для редактирования.\n'
|
||||
" Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора."
|
||||
)
|
||||
elif code == "not-removed":
|
||||
state = f"Состояние: объект «{rp}» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора."
|
||||
fix = (
|
||||
"Либо сначала снять объект с поддержки, затем удалять:\n"
|
||||
f' support-edit -Path "{rp}" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно.'
|
||||
)
|
||||
else:
|
||||
state = f"Состояние: объект «{rp}» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)."
|
||||
fix = (
|
||||
"Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):\n"
|
||||
f' support-edit -Path "{rp}" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);\n'
|
||||
f' support-edit -Path "{rp}" -Set off-support — снять с поддержки: обновления по объекту больше не приходят.'
|
||||
)
|
||||
sys.stderr.write(head + "\n" + state + "\n" + cfe + "\n" + fix + "\n" + off_note + "\n")
|
||||
sys.exit(1)
|
||||
except SystemExit:
|
||||
raise
|
||||
except Exception:
|
||||
return
|
||||
|
||||
|
||||
NSMAP = {
|
||||
"md": "http://v8.1c.ru/8.3/MDClasses",
|
||||
"v8": "http://v8.1c.ru/8.1/data/core",
|
||||
@@ -84,6 +245,7 @@ def main():
|
||||
sys.exit(1)
|
||||
|
||||
object_xml_full = os.path.abspath(object_path)
|
||||
assert_edit_allowed(object_xml_full, "editable")
|
||||
format_version = detect_format_version(os.path.dirname(object_xml_full))
|
||||
|
||||
parser_xml = etree.XMLParser(remove_blank_text=False)
|
||||
|
||||
@@ -61,7 +61,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-compile.ps1" -
|
||||
|
||||
| DSL ключ | XML элемент | Значение ключа |
|
||||
|--------------|-------------------|---------------------------------------------------|
|
||||
| `"group"` | UsualGroup | `"horizontal"` / `"vertical"` / `"alwaysHorizontal"` / `"alwaysVertical"` / `"collapsible"` |
|
||||
| `"group"` | UsualGroup | ориентация: `"vertical"` / `"horizontalIfPossible"` / `"alwaysHorizontal"` (поведение — отдельный ключ `behavior`) |
|
||||
| `"columnGroup"` | ColumnGroup | `"horizontal"` / `"vertical"` / `"inCell"` — только внутри `columns` таблицы |
|
||||
| `"input"` | InputField | имя элемента |
|
||||
| `"check"` | CheckBoxField | имя |
|
||||
@@ -83,15 +83,15 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-compile.ps1" -
|
||||
|
||||
| Ключ | Описание |
|
||||
|------|----------|
|
||||
| `name` | Переопределить имя (по умолчанию = значение ключа типа) |
|
||||
| `name` | Переопределить имя (по умолчанию = значение ключа типа). Имена уникальны во всех коллекциях формы (элементы, реквизиты, команды, колонки) |
|
||||
| `title` | Заголовок элемента |
|
||||
| `tooltip` | Всплывающая подсказка элемента (строка или `{ru,en}`) |
|
||||
| `visible: false` | Скрыть (синоним: `hidden: true`) |
|
||||
| `enabled: false` | Сделать недоступным (синоним: `disabled: true`) |
|
||||
| `readOnly: true` | Только чтение |
|
||||
| `on: [...]` | События с автоименованием обработчиков |
|
||||
| `handlers: {...}` | Явное задание имён обработчиков: `{"OnChange": "МоёИмя"}` |
|
||||
| `events: {...}` | Обработчики событий: `{ "OnChange": "ИмяОбработчика" }`. Тот же формат, что у событий формы. Значение `null` → имя обработчика сгенерируется автоматически |
|
||||
|
||||
### Допустимые имена событий (`on`)
|
||||
### Допустимые имена событий (`events`)
|
||||
|
||||
Компилятор предупреждает о неизвестных событиях. Имена регистрозависимы — используйте точно как указано.
|
||||
|
||||
@@ -116,7 +116,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-compile.ps1" -
|
||||
| Ключ | Описание | Пример |
|
||||
|------|----------|--------|
|
||||
| `path` | DataPath — привязка к данным | `"Объект.Организация"` |
|
||||
| `titleLocation` | Размещение заголовка | `"none"`, `"left"`, `"top"` |
|
||||
| `titleLocation` | Размещение заголовка | `"none"`, `"left"`, `"right"`, `"top"`, `"bottom"`, `"auto"` |
|
||||
| `multiLine: true` | Многострочное поле | текстовое поле, комментарий |
|
||||
| `passwordMode: true` | Режим пароля (звёздочки) | поле ввода пароля |
|
||||
| `choiceButton: true` | Кнопка выбора ("...") | ссылочное поле |
|
||||
@@ -179,13 +179,14 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-compile.ps1" -
|
||||
|
||||
### Группа (group)
|
||||
|
||||
Значение ключа задаёт ориентацию: `"horizontal"`, `"vertical"`, `"alwaysHorizontal"`, `"alwaysVertical"`, `"collapsible"`.
|
||||
Значение ключа задаёт **ориентацию**: `"vertical"`, `"horizontalIfPossible"`, `"alwaysHorizontal"`.
|
||||
|
||||
| Ключ | Описание |
|
||||
|------|----------|
|
||||
| `behavior` | Поведение группы: `"collapsible"` (сворачиваемая) / `"popup"` (всплывающая). Опустить = обычная |
|
||||
| `showTitle: true` | Показывать заголовок группы |
|
||||
| `united: false` | Левый край полей ввода выравнивается только в пределах этой группы (по умолчанию `true` — сквозное выравнивание по самому длинному заголовку, в т.ч. с соседними группами) |
|
||||
| `collapsed: true` | Только для `"group": "collapsible"` — группа создаётся свёрнутой |
|
||||
| `collapsed: true` | Для `behavior: "collapsible"` / `"popup"` — группа создаётся свёрнутой |
|
||||
| `representation` | `"none"`, `"normal"`, `"weak"`, `"strong"` |
|
||||
| `children: [...]` | Вложенные элементы |
|
||||
|
||||
@@ -202,8 +203,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-compile.ps1" -
|
||||
| `height` | Высота в строках таблицы |
|
||||
| `header: false` | Скрыть шапку |
|
||||
| `footer: true` | Показать подвал |
|
||||
| `commandBarLocation` | `"None"`, `"Top"`, `"Auto"` |
|
||||
| `searchStringLocation` | `"None"`, `"Top"`, `"Auto"` |
|
||||
| `commandBarLocation` | `"None"`, `"Top"`, `"Bottom"`, `"Auto"` |
|
||||
| `searchStringLocation` | `"None"`, `"Top"`, `"Bottom"`, `"CommandBar"`, `"PullFromTop"`, `"Auto"` |
|
||||
| `choiceMode: true` | Режим выбора (для форм выбора) |
|
||||
| `initialTreeView` | `"ExpandTopLevel"` и др. (иерархические списки) |
|
||||
| `enableDrag: true` | Разрешить перетаскивание |
|
||||
@@ -440,7 +441,7 @@ PictureField, привязанный к булеву/числу, рисует и
|
||||
"events": { "OnCreateAtServer": "ПриСозданииНаСервере" },
|
||||
"elements": [
|
||||
{ "group": "horizontal", "name": "ГруппаФайл", "children": [
|
||||
{ "input": "ИмяФайла", "path": "ИмяФайла", "title": "Файл", "inputHint": "Выберите файл...", "choiceButton": true, "on": ["StartChoice"] },
|
||||
{ "input": "ИмяФайла", "path": "ИмяФайла", "title": "Файл", "inputHint": "Выберите файл...", "choiceButton": true, "events": { "StartChoice": "ИмяФайлаНачалоВыбора" } },
|
||||
{ "check": "ПерваяСтрокаЗаголовок", "path": "ПерваяСтрокаЗаголовок" }
|
||||
]},
|
||||
{ "input": "Результат", "path": "Результат", "multiLine": true, "height": 8, "readOnly": true, "title": "Лог" },
|
||||
@@ -500,8 +501,8 @@ PictureField, привязанный к булеву/числу, рисует и
|
||||
"title": "Просмотр данных",
|
||||
"elements": [
|
||||
{ "group": "horizontal", "name": "Фильтр", "children": [
|
||||
{ "input": "Период", "path": "Период", "on": ["OnChange"] },
|
||||
{ "input": "Организация", "path": "Организация", "on": ["OnChange"] }
|
||||
{ "input": "Период", "path": "Период", "events": { "OnChange": "ПериодПриИзменении" } },
|
||||
{ "input": "Организация", "path": "Организация", "events": { "OnChange": "ОрганизацияПриИзменении" } }
|
||||
]},
|
||||
{ "table": "Данные", "path": "Данные", "changeRowSet": true, "columns": [
|
||||
{ "input": "Дата", "path": "Данные.Дата" },
|
||||
@@ -522,10 +523,26 @@ PictureField, привязанный к булеву/числу, рисует и
|
||||
}
|
||||
```
|
||||
|
||||
## Продвинутые конструкции (по необходимости)
|
||||
|
||||
Описанного выше хватает для большинства форм. Под конкретную задачу подгрузите файл из `references/`:
|
||||
|
||||
- `dynamic-list.md` — форма списка: источник, отбор, сортировка, группировки, параметры запроса
|
||||
- `appearance.md` — условное и статическое оформление элементов (цвета/шрифты/рамки)
|
||||
- `choice-params.md` — параметры и связи выбора у полей ввода
|
||||
- `command-interface.md` — командный интерфейс формы
|
||||
- `roles-access.md` — пользовательская видимость и доступ по ролям
|
||||
- `companion-panels.md` — контент расширенной подсказки и контекстного меню
|
||||
- `special-fields.md` — поля документа/датчика (HTML, текст, индикатор, ползунок)
|
||||
- `charts.md` — диаграммы и планировщик
|
||||
- `report-form.md` — свойства формы отчёта
|
||||
- `type-system-advanced.md` — наборы и составные типы
|
||||
- `table-advanced.md` — расширенные свойства таблиц
|
||||
- `layout-advanced.md` — тонкая компоновка и геометрия
|
||||
|
||||
## Автогенерация
|
||||
|
||||
- **Companion-элементы**: ContextMenu, ExtendedTooltip и др. создаются автоматически
|
||||
- **Обработчики событий**: `"on": ["OnChange"]` → `ОрганизацияПриИзменении`
|
||||
- **Namespace**: все 17 namespace-деклараций
|
||||
- **ID**: последовательная нумерация, AutoCommandBar = id="-1"
|
||||
- **Unknown keys**: выводится предупреждение о нераспознанных ключах
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
# Оформление
|
||||
|
||||
Два независимых механизма: **оформление элемента** (постоянные цвета/шрифт/граница на конкретном элементе) и **условное оформление формы** (`conditionalAppearance` — правила, применяемые при выполнении условия).
|
||||
|
||||
## Оформление элемента (цвета / шрифты / граница)
|
||||
|
||||
Свойства задаются прямо на элементе. Применимо к полям (`input`/`check`/`radio`/`labelField`/`picField`/`calendar`), декорациям (`label`/`picture`), кнопкам (`button`), группам (`group`/`columnGroup`), страницам (`page`/`pages`), попапам (`popup`) и таблицам (`table`). Каждое свойство необязательно.
|
||||
|
||||
| Ключ | Что задаёт |
|
||||
|------|-----------|
|
||||
| `textColor` | Цвет текста |
|
||||
| `backColor` | Цвет фона |
|
||||
| `borderColor` | Цвет рамки |
|
||||
| `font` | Шрифт |
|
||||
| `border` | Граница |
|
||||
| `titleTextColor` / `titleBackColor` / `titleFont` | Цвет текста / цвет фона / шрифт заголовка колонки (`labelField`, колонки таблицы); у `page`/`pages`/`popup` — `titleTextColor`/`titleFont` заголовка страницы/попапа |
|
||||
| `footerTextColor` / `footerBackColor` / `footerFont` | Цвет текста / цвет фона / шрифт подвала колонки |
|
||||
|
||||
Те же свойства доступны и через словарь `appearance` элемента — под русскими именами параметров платформы: `ЦветТекста`, `ЦветФона`, `ЦветРамки`, `Шрифт`, `Граница`, `ЦветТекстаЗаголовка`, `ЦветФонаЗаголовка`, `ШрифтЗаголовка`, `ЦветТекстаПодвала`, `ЦветФонаПодвала`, `ШрифтПодвала`. Это та же запись, что и в правилах условного оформления (ниже) и в `appearance` поля дин-списка.
|
||||
|
||||
### Цвет
|
||||
|
||||
Строка в одной из форм:
|
||||
|
||||
| Форма | Значение |
|
||||
|-------|----------|
|
||||
| `web:Имя` | Цвет из web-палитры, напр. `web:Red`, `web:FireBrick`, `web:HoneyDew` |
|
||||
| `win:Имя` | Системный цвет Windows, напр. `win:MenuBar`, `win:ButtonText`, `win:DisabledText` |
|
||||
| `style:ИмяСтиля` | Ссылка на элемент стиля конфигурации/платформы, напр. `style:FormBackColor`, `style:BorderColor` |
|
||||
| `#RRGGBB` | RGB-hex, напр. `#FF0000` |
|
||||
|
||||
Имя должно существовать в своей палитре (несуществующий web-/win-цвет или ссылка на отсутствующий `style:`-элемент — ошибка загрузки формы).
|
||||
|
||||
### Шрифт (`font` / `titleFont` / `footerFont`)
|
||||
|
||||
- Строка `"style:ИмяСтиля"` — шрифт из элемента стиля. Минимальная форма.
|
||||
- Объект — задаются только нужные атрибуты:
|
||||
|
||||
| Ключ | Назначение |
|
||||
|------|-----------|
|
||||
| `ref` | Ссылка на стиль (`"style:X"`) или системный шрифт (`"sys:…"`) |
|
||||
| `faceName` | Имя гарнитуры (для собственного шрифта) |
|
||||
| `height` | Размер |
|
||||
| `bold` / `italic` / `underline` / `strikeout` | `true`/`false` — начертание |
|
||||
| `scale` | Масштаб, % |
|
||||
| `kind` | `Absolute` (собственный шрифт — с `faceName`+`height`) / `WindowsFont` (системный — с `ref:"sys:…"`) |
|
||||
|
||||
```json
|
||||
{ "label": "Внимание!", "textColor": "web:FireBrick",
|
||||
"font": { "faceName": "Arial", "height": 12, "bold": true, "kind": "Absolute", "scale": 100 } }
|
||||
```
|
||||
|
||||
### Граница (`border`)
|
||||
|
||||
- Строка `"style:ИмяСтиля"` (или объект `{ "ref": "style:X" }`) — граница из стиля.
|
||||
- Объект `{ "width": N, "style": "..." }` — собственная граница. `style` — один из: `Single`, `Double`, `Underline`, `DoubleUnderline`, `Overline`, `Embossed`, `Indented`, `WithoutBorder`.
|
||||
|
||||
```json
|
||||
{ "input": "Цена", "path": "Объект.Цена", "textColor": "#FF0000",
|
||||
"borderColor": "style:BorderColor", "border": { "width": 1, "style": "Single" } }
|
||||
{ "labelField": "Код", "titleTextColor": "web:HoneyDew", "border": "style:ControlBorder" }
|
||||
```
|
||||
|
||||
## Условное оформление формы (`conditionalAppearance`)
|
||||
|
||||
Форменный ключ верхнего уровня — массив правил. Каждое правило применяет оформление к перечисленным полям, когда выполняется его условие.
|
||||
|
||||
```json
|
||||
"conditionalAppearance": [
|
||||
{ "selection": ["ОбычноеПоле"], "filter": ["ЧисловоеПоле > 100"],
|
||||
"appearance": { "ЦветФона": "style:FormBackColor" },
|
||||
"presentation": { "ru": "Подсветка", "en": "Highlight" } }
|
||||
]
|
||||
```
|
||||
|
||||
| Ключ | Тип | Назначение |
|
||||
|------|-----|-----------|
|
||||
| `selection` | array | Имена форматируемых полей формы |
|
||||
| `filter` | array | Условие применения (грамматика — ниже) |
|
||||
| `appearance` | object | Словарь «параметр платформы: значение» |
|
||||
| `presentation` | string / object | Подпись правила в списке настроек |
|
||||
| `use` | bool | `false` — правило отключено |
|
||||
| `viewMode` | string | Режим отображения настройки |
|
||||
| `userSettingID` | string | Идентификатор пользовательской настройки; `"auto"` — сгенерировать |
|
||||
|
||||
### filter
|
||||
|
||||
Та же грамматика, что в отборе списка — shorthand `"Поле оператор значение @флаги"` или объект:
|
||||
|
||||
```json
|
||||
"filter": [
|
||||
"Статус = 3",
|
||||
{ "field": "Сумма", "op": ">=", "value": 1000 },
|
||||
{ "group": "Or", "items": [ "Просрочено = true", "Заблокирован = true" ] }
|
||||
]
|
||||
```
|
||||
|
||||
- **Операторы:** `=` `<>` `>` `>=` `<` `<=`, `in` / `notIn`, `inHierarchy`, `contains` / `notContains`, `beginsWith` / `notBeginsWith`, `like` / `notLike` (`%`-шаблон), `filled` / `notFilled`.
|
||||
- **Флаги:** `@off` (отключён), `@user`, `@quickAccess`; `_` = пустое значение.
|
||||
- **Группа:** `{ "group": "And"|"Or"|"Not", "items": [...], "use"? }`.
|
||||
- **Дата-значение:** ISO-дата `"2024-01-01T00:00:00"` — фиксированная дата; именованный относительный период — строкой `"BeginningOfThisWeek"` с `"valueType": "v8:StandardBeginningDate"` (варианты `BeginningOfThisDay`/`BeginningOfThisWeek`/`BeginningOfThisMonth`/`BeginningOfThisYear`/…).
|
||||
|
||||
### appearance
|
||||
|
||||
Словарь «параметр платформы: значение». Имена параметров — русские: `ЦветТекста`, `ЦветФона`, `Шрифт`, `Граница`, `Текст`, `Заголовок`, `Формат`, `ВидимостьЭлемента`, `Доступность` и другие параметры оформления компоновки.
|
||||
|
||||
Значения:
|
||||
- **Цвет** (`ЦветТекста`/`ЦветФона`/…) и **шрифт** (`Шрифт`) — те же формы, что в оформлении элемента выше (`web:`/`win:`/`style:`/`#RRGGBB`; шрифт — строка `"style:X"` или объект).
|
||||
- **Текстовые параметры** (`Текст`/`Заголовок`/`Формат`) — по форме значения:
|
||||
- голая строка → нелокализованный литерал (`""` → пустое значение);
|
||||
- объект `{ "ru": "...", "en": "..." }` → локализуемая строка;
|
||||
- объект `{ "field": "путь" }` → ссылка на поле компоновки.
|
||||
|
||||
```json
|
||||
"conditionalAppearance": [
|
||||
{ "selection": ["Остаток"], "filter": ["Остаток < 0"],
|
||||
"appearance": { "ЦветТекста": "web:Red", "Шрифт": { "bold": true } } },
|
||||
{ "selection": ["Комментарий"], "filter": ["Комментарий notFilled"],
|
||||
"appearance": { "Текст": { "ru": "— нет данных —" }, "ЦветТекста": "win:DisabledText" } }
|
||||
]
|
||||
```
|
||||
|
||||
> Условное оформление **самого дин-списка** задаётся не здесь, а в `settings.conditionalAppearance` реквизита-списка — см. `references/dynamic-list.md`.
|
||||
@@ -0,0 +1,143 @@
|
||||
# Диаграммы, диаграмма Ганта, планировщик
|
||||
|
||||
Поле-диаграмма (`chart` / `ganttChart`), поле-планировщик (`planner`) и дендрограмма выводят значение из реквизита соответствующего типа. Конструкция всегда двойная:
|
||||
|
||||
1. **Реквизит** chart/planner-типа (несёт данные и, при необходимости, design-time конфиг).
|
||||
2. **Элемент** формы, привязанный к реквизиту через `path`.
|
||||
|
||||
Минимум — реквизит нужного типа плюс элемент с тем же `path`:
|
||||
|
||||
```json
|
||||
"attributes": [ { "name": "Диаграмма", "type": "d5p1:Chart" } ],
|
||||
"items": [ { "chart": "ПолеДиаграммы", "path": "Диаграмма" } ]
|
||||
```
|
||||
|
||||
Реквизит, заполняемый в коде (без встроенной настройки), достаточно объявить типом — элемент привязывается и работает.
|
||||
|
||||
## Типы реквизита и элемента
|
||||
|
||||
| Элемент | Ключ типа | Тип реквизита | Что несёт элемент дополнительно |
|
||||
|---------|-----------|---------------|---------------------------------|
|
||||
| Диаграмма | `chart` | `d5p1:Chart` | — |
|
||||
| Диаграмма Ганта | `ganttChart` | `d5p1:GanttChart` | `ganttTable` — вложенная таблица (см. ниже) |
|
||||
| Планировщик | `planner` | `pl:Planner` | — |
|
||||
| График. схема | `graphicalSchema` | `d5p1:FlowchartContextType` | `edit`, `warningOnEditRepresentation` |
|
||||
| Период | `periodField` | `v8:StandardPeriod` | — |
|
||||
| Дендрограмма | `dendrogram` | — | — |
|
||||
|
||||
Имя элемента — значение ключа (`"chart": "ПолеДиаграммы"`); `path` — короткое имя реквизита.
|
||||
|
||||
### Элемент диаграммы Ганта (`ganttTable`)
|
||||
|
||||
У поля Ганта внутри лежит полноценная таблица — задаётся ключом `ganttTable` (та же грамматика, что у обычной `table`):
|
||||
|
||||
```json
|
||||
{ "ganttChart": "Ганта", "path": "Ганта",
|
||||
"ganttTable": { "table": "ТаблицаГанта", "path": "Ганта", "height": 3 } }
|
||||
```
|
||||
|
||||
## Design-time конфиг диаграммы (`chart`)
|
||||
|
||||
Реквизит типа `d5p1:Chart` / `d5p1:GanttChart` может нести встроенную настройку диаграммы — объект `chart` на реквизите. Платформа всегда пишет полный набор свойств (~127: тип, серии, легенда, заголовок, шкалы, цвета, шрифты, оси), поэтому **авторинг с нуля непрактичен** — возьмите рабочую диаграмму за основу и правьте смысловое ядро.
|
||||
|
||||
Ключи `chart` = канонические имена свойств диаграммы; задавайте только те, что меняете:
|
||||
|
||||
```json
|
||||
{ "name": "Диаграмма", "type": "d5p1:Chart", "chart": {
|
||||
"chartType": "Line",
|
||||
"isSeriesDesign": true, "realSeriesCount": "2",
|
||||
"realSeriesData": [
|
||||
{ "id": "1", "color": "auto", "line": {"width":2,"gap":false,"style":"Solid"},
|
||||
"marker": "Auto", "text": "Серия 1", "strIsChanged": false, "isExpand": false,
|
||||
"isIndicator": false, "colorPriority": false }
|
||||
],
|
||||
"isShowTitle": true, "title": "Продажи",
|
||||
"isShowLegend": true, "legendPlacement": "Bottom",
|
||||
"paletteKind": "Auto"
|
||||
} }
|
||||
```
|
||||
|
||||
Смысловое ядро для правки:
|
||||
|
||||
| Ключ | Назначение |
|
||||
|------|------------|
|
||||
| `chartType` | Тип: `Line` / `Pie` / `Bar` / `Histogram` / `Column` / `Area` / … |
|
||||
| `realSeriesData` | Массив серий — объекты `{ id, text, color, line, marker, … }` |
|
||||
| `isShowTitle` + `title` | Показ и текст заголовка |
|
||||
| `isShowLegend` + `legendPlacement` | Показ и расположение легенды (`Bottom` / `Right` / …) |
|
||||
| `paletteKind` | Палитра (`Auto` / …) |
|
||||
| `bkgColor` / `labelsColor` / … | Базовые цвета |
|
||||
|
||||
Формы значений внутри `chart`:
|
||||
|
||||
- **Цвета** — verbatim: `auto`, `style:ИмяСтиля`, `web:Red`, `#hex`.
|
||||
- **`line`** — `{ width, gap, style }` (стиль линии: `Solid` / …).
|
||||
- **`border`** — `{ width, style }`.
|
||||
- **`font`** — `{ kind: "AutoFont" }` либо атрибуты шрифта.
|
||||
- **Локализуемые строки** (`title`, `vsFormat`, `lbFormat`, `labelFormat`, серия `text`, …) — голая строка либо `{ "ru": "…", "en": "…" }`.
|
||||
- **Области** (`elementsChart` / `elementsLegend` / `elementsTitle`) — `{ left, right, top, bottom }`.
|
||||
- **Серии** (`realSeriesData` / `realExSeriesData`) — массивы объектов.
|
||||
|
||||
Любое из ~127 свойств переопределяется по каноническому имени; остальное оставляйте дефолтным (не указывайте — берётся из основы).
|
||||
|
||||
### Диаграмма Ганта (`d5p1:GanttChart`)
|
||||
|
||||
Реквизит типа `d5p1:GanttChart` использует **тот же** ключ `chart`. Внутри — вложенный полный `chart`-блок плюс гант-специфика (`points` / `series` / `timeScale` / `drawEmpty` / …). Так же берите рабочую диаграмму Ганта за основу.
|
||||
|
||||
> **Ограничение.** Диаграммы (Chart/Gantt) с заполненными **точками/осями** (`realPointData` / `realDataItems`, заполненные `valuesAxis` / `pointsAxis`) генерик-движком не поддержаны — это редкий вариант. Частые дашборд-диаграммы и диаграммы Ганта (серии / легенда / оформление / шкалы) поддержаны полностью.
|
||||
|
||||
## Design-time конфиг планировщика (`planner`)
|
||||
|
||||
Реквизит типа `pl:Planner` несёт встроенную настройку планировщика — объект `planner`. Компилятор подставляет умолчания для пропущенных ключей, поэтому авторинг может быть кратким:
|
||||
|
||||
```json
|
||||
{ "name": "Планировщик", "type": "pl:Planner", "planner": {
|
||||
"items": [
|
||||
{ "text": "Встреча", "begin": "2026-06-09T01:00:00", "end": "2026-06-09T04:00:00",
|
||||
"borderColor": "auto", "backColor": "auto", "deleted": false, "editMode": "EnableEdit" }
|
||||
],
|
||||
"period": { "begin": "2026-06-09T00:00:00", "end": "2026-06-09T23:59:59" },
|
||||
"displayCurrentDate": true, "itemsTimeRepresentation": "BeginTime",
|
||||
"timeScale": { "placement": "Left", "levels": [ { "measure": "Hour", "interval": 1 } ] }
|
||||
} }
|
||||
```
|
||||
|
||||
Минимум — один `item`:
|
||||
|
||||
```json
|
||||
"planner": { "items": [ { "text": "Встреча", "begin": "2026-06-09T01:00:00", "end": "2026-06-09T04:00:00" } ] }
|
||||
```
|
||||
|
||||
| Ключ `planner` | Тип | Назначение |
|
||||
|----------------|-----|------------|
|
||||
| `items` | array | Элементы расписания. Поля элемента: `text`, `tooltip`, `begin`, `end`, `value`, `borderColor`, `backColor`, `textColor`, `font`, `border`, `replacementDate`, `deleted` (bool), `editMode` (`EnableEdit` / …), `id` (необязательно — авто-GUID), `textFormatted` |
|
||||
| `dimensions` | array | Измерения (разрезы) планировщика. Поля: `value` (объект разреза — ссылка `Enum.X.EnumValue.Y` / `Справочник.X`; опустить → пусто), `text` (заголовок), `borderColor`, `backColor`, `textColor`, `font`, `textFormatted`, `elements`. `elements` — элементы измерения, рекурсивны (могут нести вложенные `elements`): `value`, `text`, цвета, `font`, `showOnlySubordinatesAreas` (bool), `textFormatted` |
|
||||
| `period` | object | Отображаемый период `{ begin, end }` (необязательно) |
|
||||
| `timeScale` | object | Шкала времени (см. ниже) |
|
||||
| `borderColor` / `backColor` / `textColor` / `lineColor` | color | Цвета (умолч. `auto`) |
|
||||
| `font` | font | Шрифт (умолч. `{ kind: "AutoFont" }`) |
|
||||
| `border` | border | Рамка `{ width, style }` |
|
||||
| `beginOfRepresentationPeriod` / `endOfRepresentationPeriod` | dateTime | Период представления |
|
||||
| `displayCurrentDate` / `displayWrapHeaders` / `displayTimeScaleWrapHeaders` / `alignElementsOfTimeScale` | bool | Флаги отображения |
|
||||
| `timeScaleWrapHeadersFormat` | ML | Формат перенесённых заголовков шкалы |
|
||||
| `timeScaleWrapBeginIndent` / `timeScaleWrapEndIndent` | int | Отступы переноса шкалы |
|
||||
| `periodicVariantUnit` / `periodicVariantRepetition` | value / int | Единица и кратность периодического варианта |
|
||||
| `itemsTimeRepresentation` | value | Представление времени элементов (`BeginTime` / …) |
|
||||
| `itemsBehaviorWhenSpaceInsufficient` / `newItemsTextType` / `fixDimensionsHeader` / `fixTimeScaleHeader` | value | Поведение элементов и заголовков |
|
||||
| `autoMinColumnWidth` / `autoMinRowHeight` | bool | Авто-минимум размеров |
|
||||
| `minColumnWidth` / `minRowHeight` | int | Минимальные размеры |
|
||||
|
||||
Шкала времени (`timeScale`):
|
||||
|
||||
```json
|
||||
"timeScale": {
|
||||
"placement": "Left",
|
||||
"levels": [ { "measure": "Hour", "interval": 1 } ]
|
||||
}
|
||||
```
|
||||
|
||||
Ключи: `placement`, `levels` (массив уровней), `transparent`, `backColor`, `textColor`, `currentLevel`. Уровень: `measure` (`Hour` / `Day` / …), `interval`, `show`, `line` (`{ width, gap, style }`), `scaleColor`, `dayFormatRule`, `format` (ML), `labels` (`{ ticks }`), `backColor`, `textColor`, `showPereodicalLabels`.
|
||||
|
||||
Формы значений в `planner` те же, что у диаграммы: цвета verbatim (`auto` / `style:X` / `web:Red` / `#hex`); шрифт `{ kind: "AutoFont" }` либо ref-строка; граница `{ width, style }`; ML-форматы — строка или `{ "ru": …, "en": … }`.
|
||||
|
||||
> **Ограничение.** Привязка элемента расписания к элементам измерений (`item.dimensionValues`) пока всегда пустая. Сами измерения (`dimensions`) задавать можно.
|
||||
@@ -0,0 +1,73 @@
|
||||
# Параметры выбора и связь по типу
|
||||
|
||||
Свойства поля ввода (`input`), управляющие выбором значения: чем ограничен список выбора и каким будет тип значения. Имена параметров — строки 1С как есть (`"Отбор.Х"`).
|
||||
|
||||
```json
|
||||
{ "input": "Контрагент", "path": "Объект.Контрагент",
|
||||
"choiceParameters": [
|
||||
{ "name": "Отбор.Активный", "value": true },
|
||||
{ "name": "Отбор.ВидПродукции", "value": ["Enum.Виды.Агрохимикат", "Enum.Виды.Пестицид"] }
|
||||
],
|
||||
"choiceParameterLinks": [
|
||||
{ "name": "Отбор.Организация", "dataPath": "Объект.Организация" },
|
||||
{ "name": "Отбор.Тип", "dataPath": "Объект.Тип", "valueChange": "DontChange" }
|
||||
],
|
||||
"typeLink": { "dataPath": "Объект.ЗначениеДата", "linkItem": 0 }
|
||||
}
|
||||
```
|
||||
|
||||
## Параметры выбора (`choiceParameters`)
|
||||
|
||||
Фиксированные значения параметров выбора, отбирающие список значений независимо от данных формы. Массив объектов `{ name, value }`:
|
||||
|
||||
- `name` — имя параметра (`"Отбор.Активный"`).
|
||||
- `value` — значение. Допустимы: bool, число, строка, ISO-дата (`"2020-01-01T00:00:00"`), ссылка-путь (`Enum.X.Y`, `Catalog.X`). **Массив** значений задаёт фиксированный массив.
|
||||
|
||||
Короткая форма — строки `"name=value"`; значение с запятыми становится массивом, `true`/`false` → bool, число → число, остальное → строка/ссылка:
|
||||
|
||||
```json
|
||||
"choiceParameters": [
|
||||
"Отбор.Активный=true",
|
||||
"Отбор.ВидПродукции=Enum.Виды.Агрохимикат, Enum.Виды.Пестицид"
|
||||
]
|
||||
```
|
||||
|
||||
## Связи параметров выбора (`choiceParameterLinks`)
|
||||
|
||||
Параметры выбора, значение которых берётся из **другого поля формы** (а не задано фиксированно). Типовой случай — отбор списка договоров по выбранному контрагенту. Массив объектов `{ name, dataPath, valueChange? }`:
|
||||
|
||||
- `name` — имя параметра выбора.
|
||||
- `dataPath` — путь к полю формы, чьё значение подставляется в параметр.
|
||||
- `valueChange` — что делать с уже выбранным значением при смене источника: `Clear` (очистить, необязательно — поведение по умолчанию) / `DontChange` (не менять).
|
||||
|
||||
```json
|
||||
{ "input": "Договор", "path": "Объект.Договор",
|
||||
"choiceParameterLinks": [
|
||||
{ "name": "Отбор.Владелец", "dataPath": "Объект.Контрагент" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Короткая форма — строки `"name=dataPath"`, опциональный хвост `:Clear` / `:DontChange`:
|
||||
|
||||
```json
|
||||
"choiceParameterLinks": [ "Отбор.Организация=Объект.Организация", "Отбор.Тип=Объект.Тип:DontChange" ]
|
||||
```
|
||||
|
||||
## Связь по типу (`typeLink`)
|
||||
|
||||
Тип значения поля определяется другим полем формы (напр. поле «Значение» субконто, тип которого задаётся выбранным видом субконто). Объект `{ dataPath, linkItem }`:
|
||||
|
||||
- `dataPath` — путь к полю, задающему тип.
|
||||
- `linkItem` — индекс элемента связи (необязательно, по умолчанию `0`).
|
||||
|
||||
```json
|
||||
"typeLink": { "dataPath": "Объект.ВидСубконто", "linkItem": 0 }
|
||||
```
|
||||
|
||||
Короткая форма — строка `"dataPath"` либо `"dataPath#linkItem"`:
|
||||
|
||||
```json
|
||||
"typeLink": "Объект.ВидСубконто"
|
||||
"typeLink": "Объект.ВидСубконто#1"
|
||||
```
|
||||
@@ -0,0 +1,86 @@
|
||||
# Командный интерфейс формы
|
||||
|
||||
Форменный ключ `commandInterface` управляет расстановкой команд по двум панелям формы:
|
||||
|
||||
- `commandBar` — командная панель формы;
|
||||
- `navigationPanel` — панель навигации.
|
||||
|
||||
Указывать нужно **только команды, у которых меняется расстановка по умолчанию** (видимость, группа, порядок). Команды, которые платформа размещает автоматически и без изменений, в блок не включают.
|
||||
|
||||
```json
|
||||
"commandInterface": {
|
||||
"commandBar": [
|
||||
{ "command": "Form.Command.Печать", "defaultVisible": false, "group": "FormCommandBarImportant",
|
||||
"visible": { "common": false, "roles": { "Бухгалтер": true } } },
|
||||
"CommonCommand.История"
|
||||
],
|
||||
"navigationPanel": {
|
||||
"important": [ { "command": "CommonCommand.СвязанныеДокументы", "defaultVisible": false, "visible": false } ],
|
||||
"seeAlso": [ { "command": "CommonCommand.Заметки", "defaultVisible": false, "visible": false } ]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Элемент-команда
|
||||
|
||||
Каждый элемент панели — объект, либо строка-shorthand (= голый `command` со всеми остальными свойствами по умолчанию):
|
||||
|
||||
```json
|
||||
"CommonCommand.История"
|
||||
```
|
||||
|
||||
| Ключ | Тип | Назначение |
|
||||
|------|-----|-----------|
|
||||
| `command` | string | Ссылка на команду дословно: `CommonCommand.X`, `Document.X.StandardCommand.Y`, `Form.Command.X`, `Form.StandardCommand.OK`, `"0"` (пустой / разделитель) |
|
||||
| `type` | string | `Auto` (по умолчанию, необязательно) или `Added` |
|
||||
| `defaultVisible` | bool | Видимость по умолчанию. На практике задаётся только `false` — чтобы скрыть команду, которая иначе видна |
|
||||
| `visible` | bool / object | Видимость с исключениями по ролям: `bool` либо `{ "common": bool, "roles": { "Имя": bool } }` |
|
||||
| `group` | string | Группа размещения дословно: предопределённая (`FormCommandBarImportant`, `FormNavigationPanelGoTo`, …), именованная (`CommandGroup.X`) или GUID-группа расширения |
|
||||
| `index` | int | Порядок команды внутри группы |
|
||||
| `attribute` | string | Путь реквизита для элемента панели навигации |
|
||||
|
||||
## Две формы записи панели
|
||||
|
||||
Панель можно описать **плоским массивом** или **деревом по группам** — выбирайте любую.
|
||||
|
||||
**Плоский массив** — каждый элемент при необходимости несёт собственный `group`:
|
||||
|
||||
```json
|
||||
"commandBar": [
|
||||
{ "command": "Form.Command.Печать", "group": "FormCommandBarImportant", "defaultVisible": false },
|
||||
{ "command": "CommonCommand.История", "group": "FormCommandBarImportant", "index": 1 }
|
||||
]
|
||||
```
|
||||
|
||||
**Дерево** — объект `{ группа: [команды] }`; группа берётся из ключа, элементы её не повторяют:
|
||||
|
||||
```json
|
||||
"navigationPanel": {
|
||||
"important": [ "CommonCommand.СвязанныеДокументы" ],
|
||||
"goTo": [ { "command": "Document.Заказ.StandardCommand.Movements", "defaultVisible": false, "visible": false } ],
|
||||
"seeAlso": [ "CommonCommand.Заметки" ]
|
||||
}
|
||||
```
|
||||
|
||||
Ключи-группы дерева зависят от панели:
|
||||
|
||||
- `navigationPanel`: `important`, `goTo`, `seeAlso` (можно по-русски — `важное`, `перейти`, `смТакже`);
|
||||
- `commandBar`: `important`, `createBasedOn`;
|
||||
- любой другой ключ (`CommandGroup.X` или GUID) подставляется в группу дословно.
|
||||
|
||||
## Скрыть видимую команду
|
||||
|
||||
Самый частый случай — убрать команду, которую платформа показывает по умолчанию:
|
||||
|
||||
```json
|
||||
"commandBar": [
|
||||
{ "command": "Form.Command.Печать", "defaultVisible": false, "visible": false }
|
||||
]
|
||||
```
|
||||
|
||||
Показать команду только некоторым ролям:
|
||||
|
||||
```json
|
||||
{ "command": "Form.Command.Печать", "defaultVisible": false,
|
||||
"visible": { "common": false, "roles": { "Бухгалтер": true } } }
|
||||
```
|
||||
@@ -0,0 +1,131 @@
|
||||
# Companion-панели и расширенная подсказка элемента
|
||||
|
||||
Любой элемент формы может нести свой собственный контент в трёх companion-свойствах: расширенную подсказку (`extendedTooltip`), командную панель (`commandBar`) и контекстное меню (`contextMenu`). Все три задаются ключами прямо на объекте элемента.
|
||||
|
||||
```jsonc
|
||||
{ "table": "Список", "path": "Список",
|
||||
"commandBar": { "children": [ … ] },
|
||||
"contextMenu": { "children": [ … ] },
|
||||
"extendedTooltip": "Двойной клик открывает карточку" }
|
||||
```
|
||||
|
||||
## Расширенная подсказка (`extendedTooltip`)
|
||||
|
||||
Подсказка-надпись рядом с элементом. Две формы записи.
|
||||
|
||||
**Текст-форма** — просто текст подсказки:
|
||||
|
||||
```jsonc
|
||||
"extendedTooltip": "Укажите ИНН контрагента"
|
||||
"extendedTooltip": { "ru": "Сумма с НДС", "en": "Amount incl. VAT" }
|
||||
"extendedTooltip": { "text": "Всего <b>с НДС</b>", "formatted": true }
|
||||
```
|
||||
|
||||
- строка — ru-текст;
|
||||
- `{ "ru": …, "en": … }` — многоязычный (как `title`);
|
||||
- `{ "text": …, "formatted": true }` — форматированный текст (inline-разметка 1С: `<b>…</>`, `<i>`, `<u>`, `<color web:Red>…</>`, `<bgColor …>`, `<font …>`, `<fontSize …>`, `<link URL>…</>`, `<img …>`; закрывающий тег — `</>`). `formatted` нужен только когда текст содержит такую разметку.
|
||||
|
||||
**Own-content форма** — объект с раскладкой/оформлением/флагами, когда подсказке нужны размеры, цвет, гиперссылка и т.п.:
|
||||
|
||||
```jsonc
|
||||
"extendedTooltip": {
|
||||
"text": "Перейти к инструкции",
|
||||
"hyperlink": true,
|
||||
"textColor": "web:Blue",
|
||||
"events": { "URLProcessing": "ПодсказкаОбработкаНавигационнойСсылки" }
|
||||
}
|
||||
```
|
||||
|
||||
Ключи own-content объекта (все необязательны):
|
||||
|
||||
| Ключ | Тип | Назначение |
|
||||
|------|-----|-----------|
|
||||
| `text` | string/ML | Текст подсказки (с `formatted` — форматированный) |
|
||||
| `formatted` | bool | Интерпретировать inline-разметку в `text` |
|
||||
| `tooltip` | string/ML | Всплывающая подсказка самой расширенной подсказки (редко; ≠ обычному `tooltip` элемента) |
|
||||
| `hyperlink` | bool | Сделать подсказку гиперссылкой |
|
||||
| `visible` / `enabled` | bool | Видимость / доступность подсказки |
|
||||
| `width` / `height` | number | Размеры |
|
||||
| `maxWidth` / `autoMaxWidth` | number / bool | Максимальная ширина / авто-максимум |
|
||||
| `titleHeight` | number | Высота заголовка |
|
||||
| `horizontalStretch` | bool | Горизонтальное растяжение |
|
||||
| `verticalAlign` | string | Вертикальное выравнивание |
|
||||
| `textColor` / `font` | string/object | Цвет текста / шрифт (см. `references/appearance.md`) |
|
||||
| `events` | object | Обработчики событий подсказки, напр. `{ "URLProcessing": "Имя" }` у гиперссылочной подсказки |
|
||||
|
||||
## Командная панель (`commandBar`)
|
||||
|
||||
Собственная командная панель элемента (обычно таблицы или группы).
|
||||
|
||||
**Значение** — массив или объект:
|
||||
|
||||
```jsonc
|
||||
"commandBar": [ { "button": "Создать", "command": "СоздатьЭлемент" } ]
|
||||
|
||||
"commandBar": {
|
||||
"autofill": false,
|
||||
"horizontalAlign": "Right",
|
||||
"children": [
|
||||
{ "button": "Создать", "command": "СоздатьЭлемент" },
|
||||
{ "buttonGroup": "Печать", "children": [ … ] }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- массив `[ … ]` — краткая запись для `{ "children": [ … ] }`;
|
||||
- объект — `children` плюс необязательные `autofill` и `horizontalAlign`.
|
||||
|
||||
| Ключ | Тип | Назначение |
|
||||
|------|-----|-----------|
|
||||
| `children` | array | Содержимое панели — обычная грамматика кнопок (см. основную инструкцию) |
|
||||
| `autofill` | bool | `false` — подавить автозаполнение панели стандартными командами. Необязательно (по умолчанию панель автозаполняется) |
|
||||
| `horizontalAlign` | string | Горизонтальное выравнивание содержимого: `Left` / `Center` / `Right`. Необязательно |
|
||||
|
||||
`children` — кнопки: `button` (с `command` / `commandName` / `stdCommand`), `buttonGroup`, `popup` — как в основной инструкции по кнопкам.
|
||||
|
||||
> Для таблицы динамического списка панель по умолчанию подавлена (чтобы не дублировать командную панель формы). Чтобы оставить автозаполняемую панель у самой таблицы — задайте `commandBar: { "autofill": true }`.
|
||||
|
||||
## Контекстное меню (`contextMenu`)
|
||||
|
||||
Собственное контекстное меню элемента. Грамматика та же, что у `commandBar`, но без `horizontalAlign`.
|
||||
|
||||
```jsonc
|
||||
"contextMenu": [ { "button": "Карта маршрута", "commandName": "CommonCommand.КартаМаршрута" } ]
|
||||
|
||||
"contextMenu": {
|
||||
"autofill": false,
|
||||
"children": [
|
||||
{ "button": "Скопировать ссылку", "command": "СкопироватьСсылку" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
| Ключ | Тип | Назначение |
|
||||
|------|-----|-----------|
|
||||
| `children` | array | Пункты меню — обычная грамматика кнопок |
|
||||
| `autofill` | bool | `false` — подавить автозаполнение меню. Необязательно |
|
||||
|
||||
## Пример: таблица со своим меню и инфо-баннером
|
||||
|
||||
```jsonc
|
||||
{ "table": "Заказы", "path": "Объект.Заказы",
|
||||
"extendedTooltip": {
|
||||
"text": "Строки с просрочкой выделены <color web:FireBrick>красным</>",
|
||||
"formatted": true
|
||||
},
|
||||
"commandBar": {
|
||||
"autofill": false,
|
||||
"horizontalAlign": "Right",
|
||||
"children": [
|
||||
{ "button": "Добавить", "command": "ДобавитьЗаказ" },
|
||||
{ "button": "Удалить", "command": "УдалитьЗаказ" }
|
||||
]
|
||||
},
|
||||
"contextMenu": {
|
||||
"children": [
|
||||
{ "button": "Открыть документ", "command": "ОткрытьЗаказ" },
|
||||
{ "buttonGroup": "Экспорт", "children": [
|
||||
{ "button": "В Excel", "command": "ВыгрузитьВExcel" } ] }
|
||||
]
|
||||
} }
|
||||
```
|
||||
@@ -0,0 +1,144 @@
|
||||
# Динамический список
|
||||
|
||||
Реквизит с `type: "DynamicList"` (обычно `main: true`) — основа формы списка. Объект `settings` описывает источник данных и настройки списка. Минимум — указать источник:
|
||||
|
||||
```json
|
||||
{ "name": "Список", "type": "DynamicList", "main": true,
|
||||
"settings": { "mainTable": "Catalog.Контрагенты" } }
|
||||
```
|
||||
|
||||
К списку привязывается таблица-элемент (`table`), ссылающаяся на реквизит через `path` — см. основную инструкцию.
|
||||
|
||||
## Источник данных
|
||||
|
||||
Два взаимоисключающих режима:
|
||||
|
||||
**Таблично-ориентированный** — основная таблица метаданных:
|
||||
|
||||
```json
|
||||
"settings": { "mainTable": "Catalog.Контрагенты" }
|
||||
```
|
||||
|
||||
**Запросный** — произвольный запрос:
|
||||
|
||||
```json
|
||||
"settings": {
|
||||
"query": "ВЫБРАТЬ Т.Ссылка, Т.Наименование, Т.Сумма ИЗ Документ.Заказ КАК Т ГДЕ Т.Сумма > &Порог",
|
||||
"mainTable": "Document.Заказ"
|
||||
}
|
||||
```
|
||||
|
||||
| Ключ | Тип | Назначение |
|
||||
|------|-----|-----------|
|
||||
| `mainTable` | string | Основная таблица (`Catalog.X` / `Document.X` / …). Можно вместе с `query` |
|
||||
| `query` | string | Текст запроса. Поддерживает `@file.sql` (путь к файлу запроса рядом с JSON) |
|
||||
| `keyType` | string | Запросный список без `mainTable`: тип ключа набора — `FieldValue` / `RowKey` / `RowNumber` |
|
||||
| `keyFields` | array | Поля ключа набора (для `keyType` без `mainTable`) |
|
||||
|
||||
Параметры запроса (`&Имя`) задаются в `parameters` (ниже).
|
||||
|
||||
`"dynamicDataRead": false` отключает динамическое считывание (список читается обычным запросом, без фонового обновления) — нужно для тяжёлых/агрегатных запросов.
|
||||
|
||||
## Параметры запроса (`parameters`)
|
||||
|
||||
Значения для `&параметров` текста запроса. Shorthand `"Имя [Заголовок]: тип = Значение"` (всё кроме имени необязательно) либо объект:
|
||||
|
||||
```json
|
||||
"settings": {
|
||||
"query": "… ГДЕ Т.Артикул = &Артикул И Т.Цена ПОДОБНО &Маска",
|
||||
"parameters": [
|
||||
"Артикул",
|
||||
"Маска: string = %",
|
||||
{ "name": "ВидЦен", "valueListAllowed": true },
|
||||
{ "name": "Период", "type": "dateTime" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Ключи объекта: `name`, `title`, `type` (грамматика типов — см. основную инструкцию), `value`, `valueListAllowed` (разрешить список значений), `availableValues` (`[{ value, presentation }]`), `expression`, `use`.
|
||||
|
||||
## Значения параметров в настройках (`dataParameters`)
|
||||
|
||||
Предустановленные значения параметров на уровне настроек списка. Shorthand `"Имя = Значение"` или объект `{ parameter, value?, use?, viewMode? }`:
|
||||
|
||||
```json
|
||||
"dataParameters": [ "Организация = _", "ВидЦен" ]
|
||||
```
|
||||
|
||||
## Поля набора (`fields`)
|
||||
|
||||
Обычно поля выводятся из источника сами — `fields` нужен **только чтобы переопределить** свойства отдельного поля:
|
||||
|
||||
```json
|
||||
"fields": [
|
||||
{ "field": "Сумма", "title": "Сумма, руб", "appearance": { "Формат": "ЧДЦ=2" } },
|
||||
{ "field": "Остаток", "valueType": "number(15,2)" }
|
||||
]
|
||||
```
|
||||
|
||||
Ключи поля: `field`, `dataPath`, `title`, `valueType`, `appearance` (как в условном оформлении), `presentationExpression`, `inputParameters` (связь по параметрам выбора), `typeLink` (`{ field, linkItem }` — связь по типу, напр. субконто).
|
||||
|
||||
## Вычисляемые поля (`calculatedFields`)
|
||||
|
||||
Поля, считаемые выражением. Shorthand `"Имя [Заголовок]: тип = Выражение"`:
|
||||
|
||||
```json
|
||||
"calculatedFields": [
|
||||
"Метка = Code + \" \" + Description",
|
||||
"Маржа [Маржа, руб]: number(15,2) = Цена - Закупка"
|
||||
]
|
||||
```
|
||||
|
||||
Объектная форма — для `presentationExpression` / `orderExpression`:
|
||||
|
||||
```json
|
||||
{ "dataPath": "Сорт", "expression": "Code", "title": "Сорт",
|
||||
"valueType": "string(10)", "presentationExpression": "Code" }
|
||||
```
|
||||
|
||||
## Отбор (`filter`)
|
||||
|
||||
Shorthand `"Поле оператор значение @флаги"` или объект:
|
||||
|
||||
```json
|
||||
"filter": [
|
||||
"Организация = _ @off @user",
|
||||
"Сумма > 1000",
|
||||
{ "field": "Дата", "op": ">=", "value": "2024-01-01T00:00:00" },
|
||||
{ "group": "Or", "items": [ "Статус = 1", "Статус = 2" ] }
|
||||
]
|
||||
```
|
||||
|
||||
- **Операторы:** `=` `<>` `>` `>=` `<` `<=`, `in` / `notIn`, `inHierarchy`, `contains` / `notContains`, `beginsWith` / `notBeginsWith`, `like` / `notLike` (`%`-шаблон), `filled` / `notFilled`.
|
||||
- **Флаги:** `@off` (отключён), `@user` (в пользовательских настройках), `@quickAccess`; `_` = пустое значение.
|
||||
- **Группа:** `{ group: "And"|"Or"|"Not", items: [...] }`.
|
||||
- **Дата-значение:** ISO-дата `"2024-01-01T00:00:00"` — фиксированная дата. Именованный относительный период — строкой с типом: `{ "value": "BeginningOfThisWeek", "valueType": "v8:StandardBeginningDate" }` (варианты `BeginningOfThisDay`/`BeginningOfThisWeek`/`BeginningOfThisMonth`/`BeginningOfThisYear`/…).
|
||||
|
||||
## Сортировка (`order`)
|
||||
|
||||
Строка `"Поле"` (по возр.) / `"Поле desc"`, либо объект `{ field, direction? }`. `"Auto"` — автосортировка:
|
||||
|
||||
```json
|
||||
"order": [ "Дата desc", "Наименование", "Auto" ]
|
||||
```
|
||||
|
||||
## Группировка строк (`grouping`)
|
||||
|
||||
Линейная цепочка уровней (внешний → внутренний). Шорткат `>` или массив:
|
||||
|
||||
```json
|
||||
"grouping": "Контрагент > Договор"
|
||||
"grouping": [ "Контрагент", { "field": "Дата", "groupType": "Hierarchy" } ]
|
||||
```
|
||||
|
||||
Ключи уровня-объекта: `field`, `groupType` (`Items` / `Hierarchy`).
|
||||
|
||||
## Условное оформление (`conditionalAppearance`)
|
||||
|
||||
```json
|
||||
"conditionalAppearance": [
|
||||
{ "filter": [ "Просрочено = true" ], "appearance": { "ЦветТекста": "web:Red" } }
|
||||
]
|
||||
```
|
||||
|
||||
`filter` — та же грамматика, что выше. `appearance` — словарь «параметр платформы: значение» (`ЦветТекста`, `ЦветФона`, `Шрифт`, `Текст`, `Формат`, …). Значение `Текст`/`Заголовок`/`Формат`: голая строка — нелокализованный литерал; `{ru,en}` — локализуемая строка; `{ field: "путь" }` — ссылка на поле. Подробнее об оформлении — `references/appearance.md`.
|
||||
@@ -0,0 +1,111 @@
|
||||
# Продвинутая раскладка
|
||||
|
||||
Тонкая настройка размещения элемента внутри родителя сверх базовой геометрии (`width`/`height`/`horizontalStretch`/`verticalStretch`/`visible`/`enabled` и ориентации групп/страниц — они в основной инструкции). Все ключи ниже задаются прямо на элементе и **необязательны** — без них действует поведение платформы по умолчанию.
|
||||
|
||||
## Выравнивание внутри родителя
|
||||
|
||||
Различают **выравнивание самого элемента** в отведённой ему ячейке и **выравнивание содержимого** элемента.
|
||||
|
||||
| Ключ | Значения | Что выравнивает |
|
||||
|------|----------|-----------------|
|
||||
| `groupHorizontalAlign` | `Left` / `Center` / `Right` | Положение **элемента** по горизонтали в родительской группе (когда элемент у́же доступного места) |
|
||||
| `groupVerticalAlign` | `Top` / `Center` / `Bottom` | Положение **элемента** по вертикали в родительской группе |
|
||||
| `horizontalAlign` | `Left` / `Center` / `Right` | Выравнивание **содержимого** (текста/значения) внутри самого элемента |
|
||||
| `verticalAlign` | `Top` / `Center` / `Bottom` | Выравнивание содержимого по вертикали внутри элемента |
|
||||
|
||||
`group*Align` отвечает на вопрос «куда сдвинуть нерастянутый элемент в его ячейке», `horizontalAlign`/`verticalAlign` — «как разместить текст внутри элемента». Это разные оси настройки, их часто комбинируют.
|
||||
|
||||
```json
|
||||
{ "button": "ОК", "groupHorizontalAlign": "Right" }
|
||||
{ "input": "Сумма", "path": "Объект.Сумма", "horizontalAlign": "Right" }
|
||||
{ "label": "Итого", "groupHorizontalAlign": "Center", "horizontalAlign": "Center" }
|
||||
```
|
||||
|
||||
## Ограничение максимального размера
|
||||
|
||||
По умолчанию растягивающийся элемент имеет авто-вычисляемый предел ширины/высоты. Чтобы задать жёсткий предел или вовсе снять авто-предел:
|
||||
|
||||
| Ключ | Значения | Назначение |
|
||||
|------|----------|-----------|
|
||||
| `maxWidth` | число | Жёсткий максимум ширины элемента |
|
||||
| `maxHeight` | число | Жёсткий максимум высоты элемента |
|
||||
| `autoMaxWidth` | `false` | Отключить авто-предел ширины (элемент тянется без ограничения сверху) |
|
||||
| `autoMaxHeight` | `false` | Отключить авто-предел высоты |
|
||||
|
||||
`autoMaxWidth: false` нужен, например, для широкого многострочного поля или растянутого по всей форме поля ввода, чтобы платформа не «прижимала» его к авто-пределу. Указывают именно отклонение от дефолта; обычное значение `true` писать не нужно.
|
||||
|
||||
```json
|
||||
{ "input": "Комментарий", "path": "Объект.Комментарий", "multiLine": true,
|
||||
"horizontalStretch": true, "autoMaxWidth": false }
|
||||
{ "input": "Поиск", "path": "СтрокаПоиска", "horizontalStretch": true, "maxWidth": 600 }
|
||||
```
|
||||
|
||||
## Поведение при вводе и активации
|
||||
|
||||
| Ключ | Значения | Назначение |
|
||||
|------|----------|-----------|
|
||||
| `skipOnInput` | `true` / `false` | Пропускать элемент при обходе по Enter/Tab (фокус через него не проходит). Указывают явно, в т.ч. `false` чтобы вернуть в обход поле, которое платформа пропустила бы |
|
||||
| `defaultItem` | `true` | Элемент получает фокус по умолчанию при открытии формы (поле/таблица для немедленного ввода) |
|
||||
|
||||
```json
|
||||
{ "input": "Идентификатор", "path": "Объект.Идентификатор", "skipOnInput": true }
|
||||
{ "input": "Штрихкод", "path": "Штрихкод", "defaultItem": true }
|
||||
```
|
||||
|
||||
`skipOnInput: true` — для служебных/расчётных полей, которые видны, но не редактируются вводом с клавиатуры в общем потоке. `defaultItem: true` ставят на одном элементе формы — точке, с которой пользователь начнёт работу.
|
||||
|
||||
## Перетаскивание
|
||||
|
||||
| Ключ | Значения | Назначение |
|
||||
|------|----------|-----------|
|
||||
| `enableStartDrag` | `true` | Разрешить начинать перетаскивание из элемента (источник drag-n-drop) |
|
||||
|
||||
Для таблиц приём/перемещение строк управляется ключами таблицы (`enableDrag`, `changeRowOrder`) — см. основную инструкцию; `enableStartDrag` — общий низкоуровневый флаг «этот элемент может быть источником перетаскивания».
|
||||
|
||||
## Закрепление колонки в таблице (`fixingInTable`)
|
||||
|
||||
Свойство поля-колонки внутри таблицы: закрепить колонку у края, чтобы она не уходила при горизонтальной прокрутке.
|
||||
|
||||
| Значения |
|
||||
|----------|
|
||||
| `None` (по умолчанию — не закреплена) / `Left` / `Right` |
|
||||
|
||||
```json
|
||||
{ "table": "Товары", "path": "Объект.Товары", "columns": [
|
||||
{ "input": "Номенклатура", "path": "Объект.Товары.Номенклатура", "fixingInTable": "Left" },
|
||||
{ "input": "Количество", "path": "Объект.Товары.Количество" },
|
||||
{ "input": "Сумма", "path": "Объект.Товары.Сумма", "fixingInTable": "Right" } ] }
|
||||
```
|
||||
|
||||
Закрепляют ключевые колонки (идентифицирующую слева, итоговую справа), чтобы они оставались видны при прокрутке широкой таблицы.
|
||||
|
||||
## Ячейки колонок: шапка и подвал
|
||||
|
||||
Для поля-колонки внутри таблицы (и `columnGroup`) — размещение в шапке/подвале и выравнивание текста ячеек. Применять только к элементам внутри `columns` таблицы.
|
||||
|
||||
| Ключ | Значения | Назначение |
|
||||
|------|----------|-----------|
|
||||
| `showInHeader` | `true` / `false` | Показывать колонку в шапке таблицы |
|
||||
| `showInFooter` | `true` / `false` | Показывать колонку в подвале (нужно для итогов; подвал самой таблицы включается `footer: true`) |
|
||||
| `headerHorizontalAlign` | `Left` / `Right` / `Center` / `Auto` | Выравнивание текста в шапке колонки |
|
||||
| `footerHorizontalAlign` | `Left` / `Right` / `Center` | Выравнивание текста в подвале колонки |
|
||||
| `autoCellHeight` | `true` / `false` | Авто-высота ячейки (перенос содержимого на несколько строк) |
|
||||
|
||||
```json
|
||||
{ "table": "Товары", "path": "Объект.Товары", "footer": true, "columns": [
|
||||
{ "input": "Номенклатура", "path": "Объект.Товары.Номенклатура", "autoCellHeight": true },
|
||||
{ "input": "Сумма", "path": "Объект.Товары.Сумма",
|
||||
"headerHorizontalAlign": "Right", "showInFooter": true, "footerHorizontalAlign": "Right" } ] }
|
||||
```
|
||||
|
||||
## Адаптивная важность (`displayImportance`)
|
||||
|
||||
| Значения |
|
||||
|----------|
|
||||
| `VeryHigh` / `High` / `Usual` / `VeryLow` / `Low` |
|
||||
|
||||
Приоритет элемента при адаптивной перекомпоновке формы на узких/мобильных экранах: элементы с меньшей важностью сворачиваются/прячутся первыми. Применимо к любому элементу.
|
||||
|
||||
```json
|
||||
{ "input": "Комментарий", "path": "Объект.Комментарий", "displayImportance": "Low" }
|
||||
```
|
||||
@@ -0,0 +1,79 @@
|
||||
# Форма отчёта
|
||||
|
||||
Форма, подключённая к объекту-отчёту (`Report`). Кроме обычных свойств формы у неё есть несколько свойств в `properties`, связывающих форму с механизмом компоновки (СКД): куда выводится результат, где данные расшифровки, какого она типа. Все они задаются в блоке `properties` верхнего уровня.
|
||||
|
||||
```json
|
||||
"properties": {
|
||||
"reportFormType": "Main",
|
||||
"reportResult": "РезультатОтчета",
|
||||
"detailsData": "ДанныеРасшифровки"
|
||||
}
|
||||
```
|
||||
|
||||
Ни одно из этих свойств не обязательно — указывайте только те, что нужны конкретной форме.
|
||||
|
||||
## Тип формы отчёта (`reportFormType`)
|
||||
|
||||
Роль формы в составе отчёта:
|
||||
|
||||
| Значение | Назначение |
|
||||
|----------|-----------|
|
||||
| `Main` | Основная форма отчёта (результат + настройки) |
|
||||
| `Settings` | Форма настроек |
|
||||
| `Variant` | Форма варианта |
|
||||
|
||||
```json
|
||||
"reportFormType": "Main"
|
||||
```
|
||||
|
||||
## Привязка к компоновке
|
||||
|
||||
| Ключ | Тип | Назначение |
|
||||
|------|-----|-----------|
|
||||
| `reportResult` | string | Имя реквизита-результата формы (табличный документ, куда выводится отчёт) |
|
||||
| `detailsData` | string | Имя реквизита данных расшифровки |
|
||||
| `variantAppearance` | string | Имя реквизита оформления варианта |
|
||||
|
||||
Значение каждого ключа — имя реквизита формы (а не путь к данным). Реквизит с таким именем должен присутствовать в `attributes` формы.
|
||||
|
||||
## Группа пользовательских настроек (`customSettingsFolder`)
|
||||
|
||||
Группа-элемент формы, в которую генерируются пользовательские настройки компоновщика. Задаётся **по имени** элемента-группы:
|
||||
|
||||
```json
|
||||
"customSettingsFolder": "ГруппаПользовательскихНастроек"
|
||||
```
|
||||
|
||||
## Прочие свойства компоновки
|
||||
|
||||
Редкие, задавайте только при явной необходимости:
|
||||
|
||||
| Ключ | Значения | Назначение |
|
||||
|------|----------|-----------|
|
||||
| `autoShowState` | `Auto`, `DontShow`, `ShowOnComposition` | Автопоказ состояния формирования |
|
||||
| `reportResultViewMode` | `Auto` | Режим просмотра результата |
|
||||
| `viewModeApplicationOnSetReportResult` | `Auto` | Применение режима просмотра при установке результата |
|
||||
|
||||
## Реалистичный пример
|
||||
|
||||
Основная форма отчёта со СКД: реквизит-результат, данные расшифровки и группа пользовательских настроек.
|
||||
|
||||
```json
|
||||
{
|
||||
"properties": {
|
||||
"reportFormType": "Main",
|
||||
"reportResult": "РезультатОтчета",
|
||||
"detailsData": "ДанныеРасшифровки",
|
||||
"customSettingsFolder": "ГруппаПользовательскихНастроек"
|
||||
},
|
||||
"attributes": [
|
||||
{ "name": "РезультатОтчета", "type": "SpreadsheetDocument" },
|
||||
{ "name": "ДанныеРасшифровки", "type": "DataCompositionDetailsData" }
|
||||
],
|
||||
"elements": [
|
||||
{ "group": "vertical", "name": "ГруппаПользовательскихНастроек" },
|
||||
{ "spreadsheet": "РезультатОтчета", "path": "РезультатОтчета",
|
||||
"titleLocation": "none" }
|
||||
]
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,73 @@
|
||||
# Доступ по ролям
|
||||
|
||||
Единый механизм платформы для разграничения по ролям: задаётся общее значение для всех ролей плюс исключения для конкретных ролей. Один и тот же формат значения у четырёх ключей — каждый на своём владельце:
|
||||
|
||||
| Ключ | Владелец | Смысл |
|
||||
|------|----------|-------|
|
||||
| `userVisible` | элемент формы | пользовательская видимость элемента |
|
||||
| `view` | реквизит формы | право просмотра |
|
||||
| `edit` | реквизит формы | право редактирования |
|
||||
| `use` | команда формы | доступность команды |
|
||||
|
||||
Ключ необязателен: его отсутствие = полный доступ для всех ролей.
|
||||
|
||||
## Значение
|
||||
|
||||
Две формы (одинаковы для всех четырёх ключей):
|
||||
|
||||
**Скаляр** `true` / `false` — общее значение для всех ролей, без исключений:
|
||||
|
||||
```json
|
||||
{ "input": "Поле", "userVisible": false }
|
||||
```
|
||||
|
||||
**Объект** `{ "common": <bool>, "roles": { "ИмяРоли": <bool>, … } }` — общее значение `common` плюс явные исключения по ролям:
|
||||
|
||||
```json
|
||||
{ "name": "Реквизит",
|
||||
"edit": { "common": false, "roles": { "ПолныеПрава": true } } }
|
||||
```
|
||||
|
||||
Роль, **не указанная** в `roles`, наследует `common`. Указанная — задаёт явный `true`/`false` (может и совпадать с `common`).
|
||||
|
||||
## Имя роли
|
||||
|
||||
Ключи в `roles` — имена ролей конфигурации (`ПолныеПрава`, `Бухгалтер`, …).
|
||||
|
||||
## Примеры
|
||||
|
||||
Элемент скрыт у всех пользователей:
|
||||
|
||||
```json
|
||||
{ "input": "Комментарий", "userVisible": false }
|
||||
```
|
||||
|
||||
Реквизит не виден никому и редактируется только одной ролью:
|
||||
|
||||
```json
|
||||
{ "name": "СуммаБонуса",
|
||||
"view": false,
|
||||
"edit": { "common": false, "roles": { "ПолныеПрава": true } } }
|
||||
```
|
||||
|
||||
Поле доступно для просмотра всем, но редактируемо только администратору:
|
||||
|
||||
```json
|
||||
{ "name": "Статус",
|
||||
"view": true,
|
||||
"edit": { "common": false, "roles": { "Администратор": true } } }
|
||||
```
|
||||
|
||||
Команда недоступна по умолчанию, разрешена только бухгалтеру:
|
||||
|
||||
```json
|
||||
{ "name": "ПровестиЗакрытие",
|
||||
"use": { "common": false, "roles": { "Бухгалтер": true } } }
|
||||
```
|
||||
|
||||
Обратный случай — доступно всем, кроме одной роли:
|
||||
|
||||
```json
|
||||
{ "name": "РедактироватьЦену",
|
||||
"edit": { "common": true, "roles": { "Кладовщик": false } } }
|
||||
```
|
||||
@@ -0,0 +1,109 @@
|
||||
# Спец-поля «документ/датчик»
|
||||
|
||||
Поля для отображения специальных данных: табличный документ, HTML, текст, форматированный документ, индикатор, ползунок. Каждое привязывается к реквизиту своего платформенного типа.
|
||||
|
||||
Структурно это обычные поля — поддерживают общий скелет поля (`path`, `title`, `titleLocation`, флаги `readOnly`/`enabled`/`visible`, `layout`, оформление, события). Ниже — только ключ `type` (имя элемента задаётся значением ключа) и собственные скаляры каждого семейства. Все скаляры необязательны.
|
||||
|
||||
| Ключ типа | Тип реквизита |
|
||||
|-----------|---------------|
|
||||
| `spreadsheet` | `mxl:SpreadsheetDocument` (ТабличныйДокумент) |
|
||||
| `html` | `string` |
|
||||
| `textDoc` | `d5p1:TextDocument` (ТекстовыйДокумент) |
|
||||
| `formattedDoc` | `fd:FormattedDocument` (ФорматированныйДокумент) |
|
||||
| `progressBar` | число |
|
||||
| `trackBar` | число |
|
||||
|
||||
## spreadsheet — поле табличного документа
|
||||
|
||||
Просмотр/редактирование табличного документа (отчёт, печатная форма).
|
||||
|
||||
```json
|
||||
{ "spreadsheet": "ТаблицаОтчета", "path": "ТаблицаОтчета",
|
||||
"titleLocation": "none", "readOnly": true,
|
||||
"output": "Disable", "protection": true }
|
||||
```
|
||||
|
||||
| Ключ | Тип | Назначение |
|
||||
|------|-----|-----------|
|
||||
| `output` | string | Использование вывода: `Enable` / `Disable` |
|
||||
| `protection` | bool | Защита от изменений |
|
||||
| `edit` | bool | Разрешить редактирование |
|
||||
| `showGrid` | bool | Показывать сетку |
|
||||
| `showHeaders` | bool | Показывать заголовки строк/колонок |
|
||||
| `showGroups` | bool | Показывать группировки |
|
||||
| `showRowAndColumnNames` | bool | Показывать имена строк и колонок |
|
||||
| `showCellNames` | bool | Показывать имена ячеек |
|
||||
| `verticalScrollBar` / `horizontalScrollBar` | string | Режим полос прокрутки |
|
||||
| `viewScalingMode` | string | Режим масштабирования просмотра |
|
||||
| `selectionShowMode` | string | Режим отображения выделения |
|
||||
| `pointerType` | string | Тип указателя |
|
||||
| `enableDrag` / `enableStartDrag` | bool | Разрешить перетаскивание / начало перетаскивания |
|
||||
|
||||
## html — поле HTML-документа
|
||||
|
||||
Просмотр HTML. Реквизит — строка (содержит HTML-текст или адрес).
|
||||
|
||||
```json
|
||||
{ "html": "Просмотр", "path": "СодержимоеHTML", "titleLocation": "none",
|
||||
"output": "Enable", "warningOnEditRepresentation": false }
|
||||
```
|
||||
|
||||
| Ключ | Тип | Назначение |
|
||||
|------|-----|-----------|
|
||||
| `output` | string | Использование вывода: `Enable` / `Disable` |
|
||||
| `warningOnEditRepresentation` | bool | Предупреждать при изменении представления |
|
||||
|
||||
## textDoc — поле текстового документа
|
||||
|
||||
Просмотр/редактирование текстового документа.
|
||||
|
||||
```json
|
||||
{ "textDoc": "Текст", "path": "ТекстДокумента", "editMode": "Edit" }
|
||||
```
|
||||
|
||||
| Ключ | Тип | Назначение |
|
||||
|------|-----|-----------|
|
||||
| `editMode` | string | Режим редактирования (напр. `Edit` / `View`) |
|
||||
|
||||
## formattedDoc — поле форматированного документа
|
||||
|
||||
Просмотр/редактирование форматированного документа.
|
||||
|
||||
```json
|
||||
{ "formattedDoc": "Описание", "path": "ФорматированноеОписание", "editMode": "Edit" }
|
||||
```
|
||||
|
||||
| Ключ | Тип | Назначение |
|
||||
|------|-----|-----------|
|
||||
| `editMode` | string | Режим редактирования (напр. `Edit` / `View`) |
|
||||
|
||||
## progressBar — поле индикатора
|
||||
|
||||
Индикатор прогресса. Реквизит — числовой.
|
||||
|
||||
```json
|
||||
{ "progressBar": "Прогресс", "path": "Прогресс",
|
||||
"minValue": 0, "maxValue": 100, "showPercent": true }
|
||||
```
|
||||
|
||||
| Ключ | Тип | Назначение |
|
||||
|------|-----|-----------|
|
||||
| `minValue` / `maxValue` | число | Минимальное / максимальное значение |
|
||||
| `showPercent` | bool | Показывать проценты |
|
||||
|
||||
## trackBar — поле ползунка
|
||||
|
||||
Регулятор-ползунок. Реквизит — числовой.
|
||||
|
||||
```json
|
||||
{ "trackBar": "Масштаб", "path": "Масштаб",
|
||||
"minValue": 20, "maxValue": 400, "markingStep": 20 }
|
||||
```
|
||||
|
||||
| Ключ | Тип | Назначение |
|
||||
|------|-----|-----------|
|
||||
| `minValue` / `maxValue` | число | Минимальное / максимальное значение |
|
||||
| `step` | число | Шаг изменения |
|
||||
| `largeStep` | число | Крупный шаг |
|
||||
| `markingStep` | число | Шаг разметки |
|
||||
| `markingAppearance` | string | Оформление разметки |
|
||||
@@ -0,0 +1,132 @@
|
||||
# Таблица — продвинутые возможности
|
||||
|
||||
Базовый элемент таблицы (`type: "table"`, колонки, основные свойства) описан в основной инструкции, раздел «Таблица (table)». Здесь — продвинутые возможности: дополнения командной панели, специфика таблицы динамического списка и неочевидные свойства/режимы.
|
||||
|
||||
## Представление (`representation`)
|
||||
|
||||
Как таблица рисует строки:
|
||||
|
||||
```json
|
||||
{ "table": "Список", "path": "Список", "representation": "Tree" }
|
||||
```
|
||||
|
||||
`List` — плоский список (по умолчанию), `Tree` — дерево, `HierarchicalList` — иерархический список (группы + элементы на одном уровне).
|
||||
|
||||
Для дерева/иерархии управляйте раскрытием уровней через `initialTreeView` (`ExpandTopLevel` / `ExpandAllLevels` / `NoExpand`).
|
||||
|
||||
## Выделение и текущая строка
|
||||
|
||||
| Ключ | Значения | Назначение |
|
||||
|------|----------|-----------|
|
||||
| `selectionMode` | `SingleRow` / `MultiRow` | Режим выделения строк |
|
||||
| `multipleChoice` | bool | Разрешить множественный выбор (для форм выбора) |
|
||||
| `currentRowUse` | `DontUse` / `Use` / `SelectionPresentation` / `SelectionPresentationAndChoice` / `Choice` | Использование текущей строки таблицы |
|
||||
|
||||
```json
|
||||
{ "table": "Список", "path": "Список", "selectionMode": "MultiRow", "multipleChoice": true }
|
||||
```
|
||||
|
||||
## Поиск при вводе (`searchOnInput`)
|
||||
|
||||
Поведение встроенного поиска при наборе текста в таблице:
|
||||
|
||||
```json
|
||||
{ "table": "Список", "path": "Список", "searchOnInput": "Use" }
|
||||
```
|
||||
|
||||
`Auto` (по умолчанию) / `Use` (искать) / `DontUse` (не искать).
|
||||
|
||||
Где располагать сами элементы поиска — управляется `searchStringLocation` / `viewStatusLocation` / `searchControlLocation` (`None` / `Top` / `Bottom` / `CommandBar` / `Auto`).
|
||||
|
||||
## Прочие свойства таблицы
|
||||
|
||||
| Ключ | Тип | Назначение |
|
||||
|------|-----|-----------|
|
||||
| `useAlternationRowColor` | bool | Чередование цвета строк |
|
||||
| `verticalLines` / `horizontalLines` | bool | Линии сетки (укажите `false`, чтобы скрыть) |
|
||||
| `markIncomplete` | bool | Автоотметка незаполненных ячеек |
|
||||
| `heightInTableRows` | int | Высота элемента в строках (отдельно от `height`) |
|
||||
| `autoInsertNewRow` | bool | Автодобавление новой строки при вводе в последнюю |
|
||||
| `rowsPicture` | string \| object | Картинка строк. Ссылка (`"CommonPicture.X"`, `"abs:..."`) либо объект `{ src, loadTransparent?, transparentPixel? }` |
|
||||
| `tooltipRepresentation` | string | Режим показа подсказки таблицы: `None`, `Button`, `ShowBottom`, `ShowTop`, `ShowLeft`, `ShowRight`, `ShowAuto`, `Balloon` |
|
||||
|
||||
## Фиксация колонки (`fixingInTable`)
|
||||
|
||||
Свойство **колонки** (на `input` / `labelField` / `check` / `picField` внутри `columns`), а не самой таблицы. Закрепляет колонку у края при горизонтальной прокрутке:
|
||||
|
||||
```json
|
||||
{ "table": "Товары", "path": "Объект.Товары", "columns": [
|
||||
{ "input": "Номенклатура", "path": "Объект.Товары.Номенклатура", "fixingInTable": "Left" },
|
||||
{ "input": "Количество", "path": "Объект.Товары.Количество" }
|
||||
]}
|
||||
```
|
||||
|
||||
`Left` / `Right` / `None`.
|
||||
|
||||
## Исключённые команды (`excludedCommands`)
|
||||
|
||||
Убрать стандартные команды редактора таблицы (кнопки добавления/перемещения/сортировки):
|
||||
|
||||
```json
|
||||
{ "table": "Товары", "path": "Объект.Товары",
|
||||
"excludedCommands": [ "Add", "Delete", "MoveUp", "SortListAsc" ] }
|
||||
```
|
||||
|
||||
Свойство работает на любом поле и на уровне формы; для таблицы значимы команды вида `Add` / `Delete` / `MoveUp` / `MoveDown` / `SortListAsc` / `SortListDesc`.
|
||||
|
||||
## Дополнения командной панели (`additions`)
|
||||
|
||||
Дополнения — это «представления» встроенного поиска таблицы:
|
||||
|
||||
- `searchString` — отображение строки поиска,
|
||||
- `viewStatus` — состояние просмотра,
|
||||
- `searchControl` — управление поиском.
|
||||
|
||||
Каждое дополнение — полноценный элемент (полный набор свойств поля). Размещать их можно двумя способами.
|
||||
|
||||
**(1) Стандартные дополнения** генерирует платформа на уровне таблицы. В DSL указывайте **только отклонения** от стандартного вида — через карту `additions` (ключ = тип дополнения):
|
||||
|
||||
```json
|
||||
{ "table": "Список", "path": "Список",
|
||||
"additions": { "viewStatus": { "horizontalLocation": "left" } } }
|
||||
```
|
||||
|
||||
**(2) Кастомное дополнение**, размещённое прямо в командной панели — обычный элемент в `commandBar` с ключом-типом:
|
||||
|
||||
```json
|
||||
{ "table": "Список", "path": "Список", "commandBar": [
|
||||
{ "searchString": "ПоискСписка", "source": "Список", "width": 15, "horizontalStretch": true }
|
||||
]}
|
||||
```
|
||||
|
||||
- Тип-ключ: `searchString` / `viewStatus` / `searchControl`.
|
||||
- `source` — имя таблицы-источника; необязательно, по умолчанию = имя родительской таблицы.
|
||||
- `horizontalLocation`: `auto` (по умолчанию) / `left` / `right`. Применимо и к обычным элементам командных панелей.
|
||||
- Прочие свойства как у поля: `title`, `visible`, `userVisible`, `enabled`, `tooltip`, оформление, `width` / `maxWidth` / `autoMaxWidth` / `horizontalStretch` / `groupHorizontalAlign` и др.
|
||||
|
||||
## Таблица динамического списка
|
||||
|
||||
Когда `path` таблицы указывает на реквизит `type: "DynamicList"` (см. `references/dynamic-list.md`), доступен блок специфичных свойств. Указывайте **только отличия** от умолчания.
|
||||
|
||||
| Ключ | Тип | Умолчание | Назначение |
|
||||
|------|-----|-----------|-----------|
|
||||
| `rowPictureDataPath` | string | картинка осн. таблицы | Путь к картинке строки. `""` — подавить картинку |
|
||||
| `rowsPicture` | string | — | Картинка строк (`"CommonPicture.X"`) |
|
||||
| `autoRefresh` | bool | `false` | Автообновление списка |
|
||||
| `autoRefreshPeriod` | int | `60` | Период автообновления, сек |
|
||||
| `updateOnDataChange` | string | `Auto` | Обновлять при изменении данных: `Auto` / `DontUpdate` |
|
||||
| `choiceFoldersAndItems` | string | `Items` | Что выбирать: `Items` / `Folders` / `FoldersAndItems` |
|
||||
| `restoreCurrentRow` | bool | `false` | Восстанавливать текущую строку при обновлении |
|
||||
| `showRoot` | bool | `true` | Показывать корень |
|
||||
| `allowRootChoice` | bool | `false` | Разрешить выбор корня |
|
||||
| `allowGettingCurrentRowURL` | bool | `true` | Разрешить получение URL текущей строки |
|
||||
| `userSettingsGroup` | string | — | Группа пользовательских настроек (привязка к одноимённой группе настроек) |
|
||||
|
||||
```json
|
||||
{ "table": "Список", "path": "Список",
|
||||
"representation": "Tree",
|
||||
"rowPictureDataPath": "Список.DefaultPicture",
|
||||
"choiceFoldersAndItems": "FoldersAndItems",
|
||||
"allowRootChoice": true,
|
||||
"updateOnDataChange": "DontUpdate" }
|
||||
```
|
||||
@@ -0,0 +1,77 @@
|
||||
# Продвинутые конструкции типов
|
||||
|
||||
Примитивы (`string(n)`, `number(p,s)`, `boolean`, `date`/`dateTime`, …) и одиночные ссылки (`CatalogRef.Контрагенты`, `DocumentRef.Заказ`, `EnumRef.X`, …) описаны в основной инструкции. Здесь — типы, которые нельзя выразить одним именем: составные типы, наборы типов и платформенные наборы ссылок.
|
||||
|
||||
Любая из этих конструкций пишется в поле `type` реквизита, реквизита-параметра или поля.
|
||||
|
||||
## Составные типы
|
||||
|
||||
Несколько типов на одном реквизите — части перечисляются через разделитель `" | "` (можно `+`). Реквизит сможет принимать значение любого из перечисленных типов:
|
||||
|
||||
```json
|
||||
{ "name": "Плательщик",
|
||||
"type": "CatalogRef.Организации | CatalogRef.ИндивидуальныеПредприниматели" }
|
||||
```
|
||||
|
||||
Смешивать можно типы из разных категорий — ссылки, примитивы, наборы типов:
|
||||
|
||||
```json
|
||||
{ "name": "Источник",
|
||||
"type": "CatalogRef.Контрагенты | DocumentRef.Заказ | string(150)" }
|
||||
```
|
||||
|
||||
Каждая часть — самостоятельный токен из этого файла или из основной инструкции. Порядок частей произвольный.
|
||||
|
||||
## Наборы типов (TypeSet)
|
||||
|
||||
«Набор типов» подставляется вместо конкретного типа — это один токен, а не перечисление. Применимо и в составном типе как одна из частей.
|
||||
|
||||
| Токен `type` | Смысл |
|
||||
|------|-------|
|
||||
| `"DefinedType.ИмяТипа"` | определяемый тип конфигурации |
|
||||
| `"Characteristic.ИмяПлана"` | тип значения характеристики (по плану видов характеристик) |
|
||||
| `"AnyRef"` | любая ссылка |
|
||||
| `"AnyIBRef"` | любая ссылка информационной базы |
|
||||
|
||||
Определяемый тип — реквизит принимает то, что задано в определяемом типе конфигурации (например `DefinedType.ДенежнаяСумма`):
|
||||
|
||||
```json
|
||||
{ "name": "Сумма", "type": "DefinedType.ДенежнаяСумма" }
|
||||
```
|
||||
|
||||
Характеристика — тип значения берётся из плана видов характеристик:
|
||||
|
||||
```json
|
||||
{ "name": "Значение", "type": "Characteristic.ДополнительныеРеквизиты" }
|
||||
```
|
||||
|
||||
## Платформенные наборы ссылок
|
||||
|
||||
«Голый» ссылочный токен **без `.Имя`** означает «любая ссылка этой категории объектов»:
|
||||
|
||||
| Токен `type` | Смысл |
|
||||
|------|-------|
|
||||
| `"CatalogRef"` | любая ссылка справочника |
|
||||
| `"DocumentRef"` | любая ссылка документа |
|
||||
| `"EnumRef"` | любая ссылка перечисления |
|
||||
| `"ExchangePlanRef"` | любая ссылка плана обмена |
|
||||
| `"TaskRef"` | любая ссылка задачи |
|
||||
| `"BusinessProcessRef"` | любая ссылка бизнес-процесса |
|
||||
| `"ChartOfCharacteristicTypesRef"` | любая ссылка плана видов характеристик |
|
||||
| `"ChartOfAccountsRef"` | любая ссылка плана счетов |
|
||||
| `"ChartOfCalculationTypesRef"` | любая ссылка плана видов расчёта |
|
||||
|
||||
Различие с одиночной ссылкой — только в наличии `.Имя`:
|
||||
|
||||
- `"CatalogRef.Валюты"` — конкретный справочник «Валюты»;
|
||||
- `"CatalogRef"` — любой справочник.
|
||||
|
||||
```json
|
||||
{ "name": "ЛюбойСправочник", "type": "CatalogRef" }
|
||||
```
|
||||
|
||||
Эти наборы тоже комбинируются в составном типе:
|
||||
|
||||
```json
|
||||
{ "name": "Объект", "type": "CatalogRef | DocumentRef" }
|
||||
```
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,48 @@
|
||||
---
|
||||
name: form-decompile
|
||||
description: Декомпиляция управляемой формы 1С (Form.xml) в JSON-черновик в формате form-compile. Используй для scaffold новой формы по образцу или структурного рефакторинга. Не для точечных правок
|
||||
argument-hint: <FormPath> [-OutputPath <out.json>]
|
||||
disable-model-invocation: true
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Write
|
||||
- Glob
|
||||
---
|
||||
|
||||
# /form-decompile — JSON-черновик из Form.xml управляемой формы
|
||||
|
||||
Читает Form.xml и эмитит компактный JSON в формате `form-compile`. **Результат — черновик**, а не обратимое представление: см. раздел «Что получаешь».
|
||||
|
||||
## Когда использовать
|
||||
|
||||
- **Scaffold новой формы по образцу** — взять существующую форму, получить JSON, поправить и скомпилировать в новую.
|
||||
- **Структурный рефакторинг** — перебрать дерево элементов, реквизиты, команды.
|
||||
|
||||
## Когда **не** использовать
|
||||
|
||||
- **Точечные правки готовой формы** (добавить элемент, реквизит, команду) → `/form-edit`. Цикл «декомпиляция → правка JSON → компиляция» переписывает форму целиком, может терять непокрытые конструкции и даёт большой diff. `/form-edit` правит адресно.
|
||||
|
||||
## Параметры
|
||||
|
||||
| Параметр | Описание |
|
||||
|----------|----------|
|
||||
| `FormPath` | Путь к Form.xml (обязательный) |
|
||||
| `OutputPath` | Путь к выходному JSON. Если не задан — JSON в stdout |
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-decompile.ps1" -FormPath "<Form.xml>" -OutputPath "<out.json>"
|
||||
```
|
||||
|
||||
## Что получаешь
|
||||
|
||||
JSON-черновик в формате `/form-compile` — **не полное обратимое представление**: раундтрип `xml → json → xml` не гарантируется, часть конструкций DSL не покрывает и **теряет молча**.
|
||||
|
||||
Критичные конструкции (`ConditionalAppearance` со scope, design-time диаграммы/планировщики на реквизите, неизвестный тип элемента, не-Form root) → скрипт падает с ненулевым кодом и сообщением в stderr; для правок такой формы — `/form-edit`.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. `/form-decompile <Form.xml> -OutputPath draft.json` — получить черновик.
|
||||
2. Поправить JSON под задачу.
|
||||
3. `/form-compile -JsonPath draft.json -OutputPath new/Form.xml` — собрать обратно.
|
||||
4. `/form-validate` + `/form-info` — проверить результат.
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
# form-edit v1.0 — Edit 1C managed form elements
|
||||
# form-edit v1.3 — Edit 1C managed form elements
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
@@ -12,6 +12,124 @@ param(
|
||||
$ErrorActionPreference = "Stop"
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# --- Support guard (Ext/ParentConfigurations.bin) ---
|
||||
# See docs/1c-support-state-spec.md. Blocks edits of vendor objects "на замке" /
|
||||
# read-only configs unless allowed. Trigger = bin present; reaction from
|
||||
# .v8-project.json editingAllowedCheck (deny|warn|off, default deny). Never
|
||||
# throws — guard errors degrade to allow.
|
||||
function Get-RootUuid([string]$xmlPath) {
|
||||
if (-not (Test-Path $xmlPath)) { return $null }
|
||||
try {
|
||||
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
|
||||
$el = $mx.DocumentElement.FirstChild
|
||||
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
|
||||
if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } }
|
||||
} catch {}
|
||||
return $null
|
||||
}
|
||||
function Find-V8Project([string]$startDir) {
|
||||
$d = $startDir
|
||||
for ($i = 0; $i -lt 20 -and $d; $i++) {
|
||||
$pj = Join-Path $d ".v8-project.json"
|
||||
if (Test-Path $pj) { return $pj }
|
||||
$parent = [System.IO.Path]::GetDirectoryName($d)
|
||||
if ($parent -eq $d) { break }
|
||||
$d = $parent
|
||||
}
|
||||
return $null
|
||||
}
|
||||
function Get-EditMode([string]$cfgDir) {
|
||||
try {
|
||||
$pj = Find-V8Project (Get-Location).Path
|
||||
if (-not $pj) { $pj = Find-V8Project $cfgDir }
|
||||
if (-not $pj) { return 'deny' }
|
||||
$proj = Get-Content -Raw $pj | ConvertFrom-Json
|
||||
$cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/')
|
||||
if ($proj.databases) {
|
||||
foreach ($db in $proj.databases) {
|
||||
if ($db.configSrc) {
|
||||
$src = [System.IO.Path]::GetFullPath($db.configSrc).TrimEnd('\', '/')
|
||||
if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) {
|
||||
if ($db.editingAllowedCheck) { return $db.editingAllowedCheck }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($proj.editingAllowedCheck) { return $proj.editingAllowedCheck }
|
||||
return 'deny'
|
||||
} catch { return 'deny' }
|
||||
}
|
||||
function Assert-EditAllowed([string]$targetPath, [string]$require) {
|
||||
try {
|
||||
$rp = $targetPath
|
||||
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
|
||||
$elemUuid = Get-RootUuid $rp
|
||||
$cfgDir = $null; $binPath = $null
|
||||
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
|
||||
for ($i = 0; $i -lt 12 -and $d; $i++) {
|
||||
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
|
||||
if (-not $cfgDir) {
|
||||
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
|
||||
if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $cfgDir = $d; $binPath = $cand }
|
||||
}
|
||||
if ($elemUuid -and $cfgDir) { break }
|
||||
$parent = [System.IO.Path]::GetDirectoryName($d)
|
||||
if ($parent -eq $d) { break }
|
||||
$d = $parent
|
||||
}
|
||||
# New object (no element file): fall back to config root uuid.
|
||||
if (-not $elemUuid -and $cfgDir) { $elemUuid = Get-RootUuid (Join-Path $cfgDir "Configuration.xml") }
|
||||
if (-not $binPath -or -not (Test-Path $binPath)) { return }
|
||||
$bytes = [System.IO.File]::ReadAllBytes($binPath)
|
||||
if ($bytes.Length -le 32) { return }
|
||||
$start = 0
|
||||
if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 }
|
||||
$text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start)
|
||||
$hm = [regex]::Match($text, '^\{6,(\d+),(\d+),')
|
||||
if (-not $hm.Success) { return }
|
||||
$G = [int]$hm.Groups[1].Value
|
||||
$K = [int]$hm.Groups[2].Value
|
||||
if ($K -eq 0) { return }
|
||||
$best = $null
|
||||
if ($elemUuid) {
|
||||
$u = [regex]::Escape($elemUuid.ToLower())
|
||||
foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) {
|
||||
$f1 = [int]$m.Groups[1].Value
|
||||
if ($null -eq $best -or $f1 -lt $best) { $best = $f1 }
|
||||
}
|
||||
}
|
||||
$blocked = $false; $code = ""; $reason = ""
|
||||
if ($G -eq 1) { $blocked = $true; $code = "capability-off"; $reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)" }
|
||||
elseif ($require -eq 'removed') {
|
||||
if ($null -ne $best -and $best -ne 2) { $blocked = $true; $code = "not-removed"; $reason = "объект не снят с поддержки — удаление сломает обновления" }
|
||||
}
|
||||
else {
|
||||
if ($null -ne $best -and $best -eq 0) { $blocked = $true; $code = "locked"; $reason = "объект на замке — редактирование сломает обновления" }
|
||||
}
|
||||
if (-not $blocked) { return }
|
||||
$mode = Get-EditMode $cfgDir
|
||||
if ($mode -eq 'off') { return }
|
||||
# Use Console.Error (not Write-Error) — under ErrorActionPreference=Stop the
|
||||
# latter throws and would be swallowed by this function's own catch.
|
||||
if ($mode -eq 'warn') { [Console]::Error.WriteLine("[support-guard] ПРЕДУПРЕЖДЕНИЕ: $reason. Цель: $rp"); return }
|
||||
$head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления."
|
||||
$cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются."
|
||||
$offNote = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json."
|
||||
if ($code -eq "capability-off") {
|
||||
$state = "Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «$rp» редактировать нельзя."
|
||||
$fix = "Либо снять защиту явно (навык support-edit, два шага):`n 1. support-edit -Path ""$cfgDir"" -Capability on — включить возможность изменения (объекты пока остаются на замке);`n 2. support-edit -Path ""$rp"" -Set editable — открыть этот объект для редактирования.`n Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора."
|
||||
} elseif ($code -eq "not-removed") {
|
||||
$state = "Состояние: объект «$rp» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора."
|
||||
$fix = "Либо сначала снять объект с поддержки, затем удалять:`n support-edit -Path ""$rp"" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно."
|
||||
} else {
|
||||
$state = "Состояние: объект «$rp» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)."
|
||||
$fix = "Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):`n support-edit -Path ""$rp"" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);`n support-edit -Path ""$rp"" -Set off-support — снять с поддержки: обновления по объекту больше не приходят."
|
||||
}
|
||||
[Console]::Error.WriteLine("$head`n$state`n$cfe`n$fix`n$offNote")
|
||||
exit 1
|
||||
} catch { return }
|
||||
}
|
||||
|
||||
# === 1. Load Form.xml ===
|
||||
|
||||
if (-not (Test-Path $FormPath)) {
|
||||
@@ -24,6 +142,7 @@ if (-not (Test-Path $JsonPath)) {
|
||||
}
|
||||
|
||||
$resolvedFormPath = (Resolve-Path $FormPath).Path
|
||||
Assert-EditAllowed $resolvedFormPath 'editable'
|
||||
$xmlDoc = New-Object System.Xml.XmlDocument
|
||||
$xmlDoc.PreserveWhitespace = $true
|
||||
try {
|
||||
@@ -291,6 +410,16 @@ function Get-ElementName {
|
||||
return "$($el.$typeKey)"
|
||||
}
|
||||
|
||||
# Уникальность имён внутри JSON-определения (1С: своя коллекция — свой неймспейс).
|
||||
function Assert-EditUnique {
|
||||
param([string]$name, [hashtable]$seen, [string]$ctx)
|
||||
if ($seen.ContainsKey($name)) {
|
||||
Write-Host "[ERROR] Duplicate $ctx '$name' in JSON definition — names must be unique in 1C form"
|
||||
exit 1
|
||||
}
|
||||
$seen[$name] = $true
|
||||
}
|
||||
|
||||
$script:knownEvents = @{
|
||||
"input" = @("OnChange","StartChoice","ChoiceProcessing","AutoComplete","TextEditEnd","Clearing","Creating","EditTextChange")
|
||||
"check" = @("OnChange")
|
||||
@@ -864,17 +993,31 @@ if ($def.elements -and $def.elements.Count -gt 0) {
|
||||
# Detect indent level
|
||||
$childIndent = Get-ChildIndent $targetCI
|
||||
|
||||
# Check for duplicate element names
|
||||
# Имена элементов уникальны (требование 1С). Сначала — внутри самого JSON-определения
|
||||
# (рекурсивно по children/columns).
|
||||
$elemTypeKeys = @("group","input","check","label","labelField","table","pages","page","button","picture","picField","calendar","cmdBar","popup")
|
||||
function Walk-ElemNames($el, [hashtable]$seen) {
|
||||
$tk = $null
|
||||
foreach ($k in $elemTypeKeys) { if ($el.$k -ne $null) { $tk = $k; break } }
|
||||
if ($tk) { Assert-EditUnique -name (Get-ElementName -el $el -typeKey $tk) -seen $seen -ctx 'element name' }
|
||||
if ($el.children) { foreach ($c in $el.children) { Walk-ElemNames $c $seen } }
|
||||
if ($el.columns) { foreach ($c in $el.columns) { Walk-ElemNames $c $seen } }
|
||||
}
|
||||
$dslElemNames = @{}
|
||||
foreach ($el in $def.elements) { Walk-ElemNames $el $dslElemNames }
|
||||
|
||||
# Затем — против уже существующих элементов формы (дубль = битый XML, форма не откроется)
|
||||
foreach ($el in $def.elements) {
|
||||
$typeKey = $null
|
||||
foreach ($key in @("group","input","check","label","labelField","table","pages","page","button","picture","picField","calendar","cmdBar","popup")) {
|
||||
foreach ($key in $elemTypeKeys) {
|
||||
if ($el.$key -ne $null) { $typeKey = $key; break }
|
||||
}
|
||||
if ($typeKey) {
|
||||
$elName = Get-ElementName -el $el -typeKey $typeKey
|
||||
$existing = Find-Element $rootCI $elName
|
||||
if ($existing) {
|
||||
Write-Host "[WARN] Element '$elName' already exists in form (id=$($existing.GetAttribute('id')))"
|
||||
Write-Host "[ERROR] Element '$elName' already exists in form (id=$($existing.GetAttribute('id'))) — element names must be unique"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -953,6 +1096,22 @@ if ($def.attributes -and $def.attributes.Count -gt 0) {
|
||||
$attrChildIndent = Get-ChildIndent $attrsSection
|
||||
if (-not $attrChildIndent -or $attrChildIndent -eq "") { $attrChildIndent = "`t`t" }
|
||||
|
||||
# Уникальность имён реквизитов: внутри JSON-определения (+ колонки в пределах реквизита) и
|
||||
# против уже существующих реквизитов формы.
|
||||
$dslAttrNames = @{}
|
||||
foreach ($attr in $def.attributes) {
|
||||
Assert-EditUnique -name "$($attr.name)" -seen $dslAttrNames -ctx 'attribute name'
|
||||
if ($attr.columns) {
|
||||
$dslColNames = @{}
|
||||
foreach ($col in $attr.columns) { Assert-EditUnique -name "$($col.name)" -seen $dslColNames -ctx "column name of '$($attr.name)'" }
|
||||
}
|
||||
$existingAttr = $attrsSection.SelectSingleNode("f:Attribute[@name='$($attr.name)']", $nsMgr)
|
||||
if ($existingAttr) {
|
||||
Write-Host "[ERROR] Attribute '$($attr.name)' already exists in form — attribute names must be unique"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# Generate attribute fragments
|
||||
$script:xml = New-Object System.Text.StringBuilder 2048
|
||||
X "<_F $allNsDecl>"
|
||||
@@ -1021,6 +1180,17 @@ if ($def.commands -and $def.commands.Count -gt 0) {
|
||||
$cmdChildIndent = Get-ChildIndent $cmdsSection
|
||||
if (-not $cmdChildIndent -or $cmdChildIndent -eq "") { $cmdChildIndent = "`t`t" }
|
||||
|
||||
# Уникальность имён команд: внутри JSON-определения и против существующих команд формы.
|
||||
$dslCmdNames = @{}
|
||||
foreach ($cmd in $def.commands) {
|
||||
Assert-EditUnique -name "$($cmd.name)" -seen $dslCmdNames -ctx 'command name'
|
||||
$existingCmd = $cmdsSection.SelectSingleNode("f:Command[@name='$($cmd.name)']", $nsMgr)
|
||||
if ($existingCmd) {
|
||||
Write-Host "[ERROR] Command '$($cmd.name)' already exists in form — command names must be unique"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# Generate command fragments
|
||||
$script:xml = New-Object System.Text.StringBuilder 1024
|
||||
X "<_F $allNsDecl>"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# form-edit v1.0 — Edit 1C managed form elements (Python port)
|
||||
# form-edit v1.3 — Edit 1C managed form elements (Python port)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
import argparse
|
||||
import json
|
||||
@@ -11,6 +11,165 @@ from lxml import etree
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
sys.stderr.reconfigure(encoding="utf-8")
|
||||
|
||||
# ============================================================
|
||||
# Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md
|
||||
# Blocks edits of vendor objects "на замке" / read-only configs. Trigger = bin
|
||||
# present; reaction from .v8-project.json editingAllowedCheck (deny|warn|off,
|
||||
# default deny). Never throws (except sys.exit on deny) — errors degrade to allow.
|
||||
# ============================================================
|
||||
|
||||
def _sg_root_uuid(xml_path):
|
||||
if not os.path.isfile(xml_path):
|
||||
return None
|
||||
try:
|
||||
mx = etree.parse(xml_path).getroot()
|
||||
for child in mx:
|
||||
if isinstance(child.tag, str) and child.get("uuid"):
|
||||
return child.get("uuid")
|
||||
except Exception:
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def _sg_find_v8project(start_dir):
|
||||
d = start_dir
|
||||
for _ in range(20):
|
||||
if not d:
|
||||
break
|
||||
pj = os.path.join(d, ".v8-project.json")
|
||||
if os.path.isfile(pj):
|
||||
return pj
|
||||
parent = os.path.dirname(d)
|
||||
if parent == d:
|
||||
break
|
||||
d = parent
|
||||
return None
|
||||
|
||||
|
||||
def _sg_get_edit_mode(cfg_dir):
|
||||
try:
|
||||
pj = _sg_find_v8project(os.getcwd()) or _sg_find_v8project(cfg_dir)
|
||||
if not pj:
|
||||
return "deny"
|
||||
proj = json.loads(open(pj, encoding="utf-8-sig").read())
|
||||
cfg_full = os.path.normcase(os.path.abspath(cfg_dir)).rstrip("\\/")
|
||||
for db in proj.get("databases", []):
|
||||
src = db.get("configSrc")
|
||||
if src:
|
||||
src_full = os.path.normcase(os.path.abspath(src)).rstrip("\\/")
|
||||
if cfg_full == src_full or cfg_full.startswith(src_full + os.sep):
|
||||
if db.get("editingAllowedCheck"):
|
||||
return db["editingAllowedCheck"]
|
||||
if proj.get("editingAllowedCheck"):
|
||||
return proj["editingAllowedCheck"]
|
||||
return "deny"
|
||||
except Exception:
|
||||
return "deny"
|
||||
|
||||
|
||||
def assert_edit_allowed(target_path, require):
|
||||
try:
|
||||
rp = os.path.abspath(target_path)
|
||||
elem_uuid = _sg_root_uuid(rp)
|
||||
cfg_dir = None
|
||||
bin_path = None
|
||||
d = rp if os.path.isdir(rp) else os.path.dirname(rp)
|
||||
for _ in range(12):
|
||||
if not d:
|
||||
break
|
||||
if not elem_uuid:
|
||||
elem_uuid = _sg_root_uuid(d + ".xml")
|
||||
if not cfg_dir:
|
||||
cand = os.path.join(d, "Ext", "ParentConfigurations.bin")
|
||||
if os.path.exists(cand) or os.path.exists(os.path.join(d, "Configuration.xml")):
|
||||
cfg_dir = d
|
||||
bin_path = cand
|
||||
if elem_uuid and cfg_dir:
|
||||
break
|
||||
parent = os.path.dirname(d)
|
||||
if parent == d:
|
||||
break
|
||||
d = parent
|
||||
if not elem_uuid and cfg_dir:
|
||||
elem_uuid = _sg_root_uuid(os.path.join(cfg_dir, "Configuration.xml"))
|
||||
if not bin_path or not os.path.exists(bin_path):
|
||||
return
|
||||
data = open(bin_path, "rb").read()
|
||||
if len(data) <= 32:
|
||||
return
|
||||
if data[:3] == b"\xef\xbb\xbf":
|
||||
data = data[3:]
|
||||
text = data.decode("utf-8", "replace")
|
||||
h = re.match(r"\{6,(\d+),(\d+),", text)
|
||||
if not h:
|
||||
return
|
||||
g = int(h.group(1))
|
||||
k = int(h.group(2))
|
||||
if k == 0:
|
||||
return
|
||||
best = None
|
||||
if elem_uuid:
|
||||
for m in re.finditer(r"([0-2]),0," + re.escape(elem_uuid.lower()), text):
|
||||
f1 = int(m.group(1))
|
||||
if best is None or f1 < best:
|
||||
best = f1
|
||||
blocked = False
|
||||
code = ""
|
||||
reason = ""
|
||||
if g == 1:
|
||||
blocked = True
|
||||
code = "capability-off"
|
||||
reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)"
|
||||
elif require == "removed":
|
||||
if best is not None and best != 2:
|
||||
blocked = True
|
||||
code = "not-removed"
|
||||
reason = "объект не снят с поддержки — удаление сломает обновления"
|
||||
else:
|
||||
if best is not None and best == 0:
|
||||
blocked = True
|
||||
code = "locked"
|
||||
reason = "объект на замке — редактирование сломает обновления"
|
||||
if not blocked:
|
||||
return
|
||||
mode = _sg_get_edit_mode(cfg_dir)
|
||||
if mode == "off":
|
||||
return
|
||||
if mode == "warn":
|
||||
sys.stderr.write(f"[support-guard] ПРЕДУПРЕЖДЕНИЕ: {reason}. Цель: {rp}\n")
|
||||
return
|
||||
head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления."
|
||||
cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются."
|
||||
off_note = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json."
|
||||
if code == "capability-off":
|
||||
state = f"Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «{rp}» редактировать нельзя."
|
||||
fix = (
|
||||
"Либо снять защиту явно (навык support-edit, два шага):\n"
|
||||
f' 1. support-edit -Path "{cfg_dir}" -Capability on — включить возможность изменения (объекты пока остаются на замке);\n'
|
||||
f' 2. support-edit -Path "{rp}" -Set editable — открыть этот объект для редактирования.\n'
|
||||
" Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора."
|
||||
)
|
||||
elif code == "not-removed":
|
||||
state = f"Состояние: объект «{rp}» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора."
|
||||
fix = (
|
||||
"Либо сначала снять объект с поддержки, затем удалять:\n"
|
||||
f' support-edit -Path "{rp}" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно.'
|
||||
)
|
||||
else:
|
||||
state = f"Состояние: объект «{rp}» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)."
|
||||
fix = (
|
||||
"Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):\n"
|
||||
f' support-edit -Path "{rp}" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);\n'
|
||||
f' support-edit -Path "{rp}" -Set off-support — снять с поддержки: обновления по объекту больше не приходят.'
|
||||
)
|
||||
sys.stderr.write(head + "\n" + state + "\n" + cfe + "\n" + fix + "\n" + off_note + "\n")
|
||||
sys.exit(1)
|
||||
except SystemExit:
|
||||
raise
|
||||
except Exception:
|
||||
return
|
||||
|
||||
|
||||
# ── arg parsing ──────────────────────────────────────────────
|
||||
|
||||
parser = argparse.ArgumentParser(allow_abbrev=False)
|
||||
@@ -63,6 +222,7 @@ if not os.path.exists(json_path):
|
||||
sys.exit(1)
|
||||
|
||||
resolved_form_path = os.path.abspath(form_path)
|
||||
assert_edit_allowed(resolved_form_path, "editable")
|
||||
xml_parser = etree.XMLParser(remove_blank_text=False)
|
||||
try:
|
||||
tree = etree.parse(resolved_form_path, xml_parser)
|
||||
@@ -365,6 +525,14 @@ def get_element_name(el, type_key):
|
||||
return str(el[type_key])
|
||||
|
||||
|
||||
def _assert_edit_unique(name, seen, ctx):
|
||||
# Уникальность имён внутри JSON-определения (1С: своя коллекция — свой неймспейс).
|
||||
if name in seen:
|
||||
print(f"[ERROR] Duplicate {ctx} '{name}' in JSON definition — names must be unique in 1C form")
|
||||
sys.exit(1)
|
||||
seen.add(name)
|
||||
|
||||
|
||||
known_events = {
|
||||
"input": ["OnChange", "StartChoice", "ChoiceProcessing", "AutoComplete", "TextEditEnd", "Clearing", "Creating", "EditTextChange"],
|
||||
"check": ["OnChange"],
|
||||
@@ -988,7 +1156,26 @@ if elements_list:
|
||||
# Detect indent level
|
||||
child_indent = get_child_indent(target_ci)
|
||||
|
||||
# Check for duplicate element names
|
||||
# Имена элементов уникальны (требование 1С). Сначала — внутри самого JSON-определения
|
||||
# (рекурсивно по children/columns).
|
||||
def _walk_elem_names(el, seen):
|
||||
tk = None
|
||||
for key in ELEMENT_KEYS:
|
||||
if key in el and el[key] is not None:
|
||||
tk = key
|
||||
break
|
||||
if tk:
|
||||
_assert_edit_unique(get_element_name(el, tk), seen, "element name")
|
||||
for c in el.get("children", []):
|
||||
_walk_elem_names(c, seen)
|
||||
for c in el.get("columns", []):
|
||||
_walk_elem_names(c, seen)
|
||||
|
||||
dsl_elem_names = set()
|
||||
for el in elements_list:
|
||||
_walk_elem_names(el, dsl_elem_names)
|
||||
|
||||
# Затем — против уже существующих элементов формы (дубль = битый XML, форма не откроется).
|
||||
for el in elements_list:
|
||||
type_key = None
|
||||
for key in ELEMENT_KEYS:
|
||||
@@ -999,7 +1186,8 @@ if elements_list:
|
||||
el_name = get_element_name(el, type_key)
|
||||
existing = find_element(root_ci, el_name) if root_ci is not None else None
|
||||
if existing is not None:
|
||||
print(f"[WARN] Element '{el_name}' already exists in form (id={existing.get('id')})")
|
||||
print(f"[ERROR] Element '{el_name}' already exists in form (id={existing.get('id')}) — element names must be unique")
|
||||
sys.exit(1)
|
||||
|
||||
# Remember starting element ID for companion counting
|
||||
start_elem_id = next_elem_id
|
||||
@@ -1055,6 +1243,19 @@ if attrs_list:
|
||||
if not attr_child_indent:
|
||||
attr_child_indent = "\t\t"
|
||||
|
||||
# Уникальность имён реквизитов: внутри JSON-определения (+ колонки в пределах реквизита) и
|
||||
# против уже существующих реквизитов формы.
|
||||
dsl_attr_names = set()
|
||||
for attr in attrs_list:
|
||||
_assert_edit_unique(str(attr["name"]), dsl_attr_names, "attribute name")
|
||||
if attr.get("columns"):
|
||||
dsl_col_names = set()
|
||||
for col in attr["columns"]:
|
||||
_assert_edit_unique(str(col["name"]), dsl_col_names, f"column name of '{attr['name']}'")
|
||||
if attrs_section.find(f"f:Attribute[@name='{attr['name']}']", NS) is not None:
|
||||
print(f"[ERROR] Attribute '{attr['name']}' already exists in form — attribute names must be unique")
|
||||
sys.exit(1)
|
||||
|
||||
# Generate attribute fragments
|
||||
xml_lines.clear()
|
||||
X(f"<_F {ALL_NS_DECL}>")
|
||||
@@ -1116,6 +1317,14 @@ if cmds_list:
|
||||
if not cmd_child_indent:
|
||||
cmd_child_indent = "\t\t"
|
||||
|
||||
# Уникальность имён команд: внутри JSON-определения и против существующих команд формы.
|
||||
dsl_cmd_names = set()
|
||||
for cmd in cmds_list:
|
||||
_assert_edit_unique(str(cmd["name"]), dsl_cmd_names, "command name")
|
||||
if cmds_section.find(f"f:Command[@name='{cmd['name']}']", NS) is not None:
|
||||
print(f"[ERROR] Command '{cmd['name']}' already exists in form — command names must be unique")
|
||||
sys.exit(1)
|
||||
|
||||
xml_lines.clear()
|
||||
X(f"<_F {ALL_NS_DECL}>")
|
||||
for cmd in cmds_list:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# form-info v1.3 — Analyze 1C managed form structure
|
||||
# form-info v1.4 — Analyze 1C managed form structure
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory=$true)]
|
||||
@@ -368,6 +368,69 @@ if ($formsIdx -ge 0 -and ($formsIdx + 1) -lt $parts.Count) {
|
||||
}
|
||||
}
|
||||
|
||||
# --- Support status (Ext/ParentConfigurations.bin) ---
|
||||
# See docs/1c-support-state-spec.md. Walks up from the target path, taking the
|
||||
# uuid of the nearest element meta-xml (form/template/etc.) and the config root
|
||||
# bin. Never throws — degrades to "не на поддержке".
|
||||
function Get-SupportStatusForPath([string]$targetPath) {
|
||||
try {
|
||||
$rp = (Resolve-Path $targetPath).Path
|
||||
$elemUuid = $null
|
||||
$binPath = $null
|
||||
# Reads the uuid of the first metadata element in an .xml file (or $null).
|
||||
function Get-RootUuid([string]$xmlPath) {
|
||||
if (-not (Test-Path $xmlPath)) { return $null }
|
||||
try {
|
||||
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
|
||||
$el = $mx.DocumentElement.FirstChild
|
||||
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
|
||||
if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } }
|
||||
} catch {}
|
||||
return $null
|
||||
}
|
||||
# The target file itself may be the element meta-xml (e.g. Subsystems/X.xml).
|
||||
$elemUuid = Get-RootUuid $rp
|
||||
$d = [System.IO.Path]::GetDirectoryName($rp)
|
||||
for ($i = 0; $i -lt 12 -and $d; $i++) {
|
||||
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
|
||||
if (-not $binPath) {
|
||||
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
|
||||
if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $binPath = $cand }
|
||||
}
|
||||
if ($elemUuid -and $binPath) { break }
|
||||
$parent = [System.IO.Path]::GetDirectoryName($d)
|
||||
if ($parent -eq $d) { break }
|
||||
$d = $parent
|
||||
}
|
||||
if (-not $binPath -or -not (Test-Path $binPath)) { return "не на поддержке" }
|
||||
$bytes = [System.IO.File]::ReadAllBytes($binPath)
|
||||
if ($bytes.Length -le 32) { return "снято с поддержки (правки свободны)" }
|
||||
$start = 0
|
||||
if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 }
|
||||
$text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start)
|
||||
$h = [regex]::Match($text, '^\{6,(\d+),(\d+),')
|
||||
if (-not $h.Success) { return "не на поддержке" }
|
||||
$G = [int]$h.Groups[1].Value
|
||||
$K = [int]$h.Groups[2].Value
|
||||
if ($K -eq 0) { return "снято с поддержки (правки свободны)" }
|
||||
if ($G -eq 1) { return "конфигурация read-only (возможность изменения выключена) — правки невозможны без включения" }
|
||||
if (-not $elemUuid) { return "не на поддержке" }
|
||||
$u = [regex]::Escape($elemUuid.ToLower())
|
||||
$best = $null
|
||||
foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) {
|
||||
$f1 = [int]$m.Groups[1].Value
|
||||
if ($null -eq $best -or $f1 -lt $best) { $best = $f1 }
|
||||
}
|
||||
if ($null -eq $best) { return "не на поддержке" }
|
||||
switch ($best) {
|
||||
0 { return "на замке — прямая правка сломает обновления; дорабатывай через cfe-* либо включи редактирование объекта" }
|
||||
1 { return "редактируется с сохранением поддержки" }
|
||||
2 { return "снято с поддержки (правки свободны)" }
|
||||
}
|
||||
return "не на поддержке"
|
||||
} catch { return "не на поддержке" }
|
||||
}
|
||||
|
||||
# --- Collect output ---
|
||||
|
||||
$lines = @()
|
||||
@@ -385,6 +448,7 @@ if ($formTitle) { $header += " — `"$formTitle`"" }
|
||||
if ($objectContext) { $header += " ($objectContext)" }
|
||||
$header += " ==="
|
||||
$lines += $header
|
||||
$lines += "Поддержка: $(Get-SupportStatusForPath $FormPath)"
|
||||
|
||||
# --- Form properties (Title excluded — shown in header) ---
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# form-info v1.3 — Analyze 1C managed form structure
|
||||
# form-info v1.4 — Analyze 1C managed form structure
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
@@ -336,6 +336,78 @@ def build_tree(child_items_node, prefix, tree_lines, expand="", state=None):
|
||||
build_tree(ci, prefix + continuation, tree_lines, expand, state)
|
||||
|
||||
|
||||
# --- Support status (Ext/ParentConfigurations.bin) ---
|
||||
# See docs/1c-support-state-spec.md. Walks up from the target path, taking the
|
||||
# uuid of the nearest element meta-xml and the config root bin. Never throws —
|
||||
# degrades to "не на поддержке".
|
||||
def get_support_status_for_path(target_path):
|
||||
try:
|
||||
def root_uuid(xml_path):
|
||||
if not os.path.isfile(xml_path):
|
||||
return None
|
||||
try:
|
||||
mx = etree.parse(xml_path).getroot()
|
||||
for child in mx:
|
||||
if isinstance(child.tag, str) and child.get("uuid"):
|
||||
return child.get("uuid")
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
rp = os.path.abspath(target_path)
|
||||
# The target file itself may be the element meta-xml (e.g. Subsystems/X.xml).
|
||||
elem_uuid = root_uuid(rp)
|
||||
bin_path = None
|
||||
d = os.path.dirname(rp)
|
||||
for _ in range(12):
|
||||
if not d:
|
||||
break
|
||||
if not elem_uuid:
|
||||
elem_uuid = root_uuid(d + ".xml")
|
||||
if not bin_path:
|
||||
cand = os.path.join(d, "Ext", "ParentConfigurations.bin")
|
||||
if os.path.exists(cand) or os.path.exists(os.path.join(d, "Configuration.xml")):
|
||||
bin_path = cand
|
||||
if elem_uuid and bin_path:
|
||||
break
|
||||
parent = os.path.dirname(d)
|
||||
if parent == d:
|
||||
break
|
||||
d = parent
|
||||
if not bin_path or not os.path.exists(bin_path):
|
||||
return "не на поддержке"
|
||||
data = open(bin_path, "rb").read()
|
||||
if len(data) <= 32:
|
||||
return "снято с поддержки (правки свободны)"
|
||||
if data[:3] == b"\xef\xbb\xbf":
|
||||
data = data[3:]
|
||||
text = data.decode("utf-8", "replace")
|
||||
h = re.match(r"\{6,(\d+),(\d+),", text)
|
||||
if not h:
|
||||
return "не на поддержке"
|
||||
g = int(h.group(1))
|
||||
k = int(h.group(2))
|
||||
if k == 0:
|
||||
return "снято с поддержки (правки свободны)"
|
||||
if g == 1:
|
||||
return "конфигурация read-only (возможность изменения выключена) — правки невозможны без включения"
|
||||
if not elem_uuid:
|
||||
return "не на поддержке"
|
||||
best = None
|
||||
for m in re.finditer(r"([0-2]),0," + re.escape(elem_uuid.lower()), text):
|
||||
f1 = int(m.group(1))
|
||||
if best is None or f1 < best:
|
||||
best = f1
|
||||
if best is None:
|
||||
return "не на поддержке"
|
||||
return {
|
||||
0: "на замке — прямая правка сломает обновления; дорабатывай через cfe-* либо включи редактирование объекта",
|
||||
1: "редактируется с сохранением поддержки",
|
||||
2: "снято с поддержки (правки свободны)",
|
||||
}.get(best, "не на поддержке")
|
||||
except Exception:
|
||||
return "не на поддержке"
|
||||
|
||||
|
||||
# --- Main ---
|
||||
|
||||
def main():
|
||||
@@ -441,6 +513,7 @@ def main():
|
||||
header += f" ({object_context})"
|
||||
header += " ==="
|
||||
lines.append(header)
|
||||
lines.append(f"Поддержка: {get_support_status_for_path(form_path)}")
|
||||
|
||||
# --- Form properties (Title excluded -- shown in header) ---
|
||||
prop_names = [
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# form-validate v1.6 — Validate 1C managed form
|
||||
# form-validate v1.7 — Validate 1C managed form
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
@@ -155,7 +155,8 @@ if (-not $stopped) {
|
||||
|
||||
# --- Collect all elements with IDs ---
|
||||
|
||||
$elementIds = @{} # id -> name (element ID pool)
|
||||
$elementIds = @{} # id -> name (element ID pool)
|
||||
$elementNames = @{} # name -> id (имена элементов уникальны в пределах формы)
|
||||
$allElements = @() # @{Name; Tag; Id; ParentName; Node}
|
||||
|
||||
function Collect-Elements {
|
||||
@@ -185,6 +186,13 @@ function Collect-Elements {
|
||||
} else {
|
||||
$elementIds[$id] = $name
|
||||
}
|
||||
|
||||
# Имена элементов уникальны (требование 1С)
|
||||
if ($elementNames.ContainsKey($name)) {
|
||||
Report-Error "Duplicate element name '$name': id=${id} and id=$($elementNames[$name])"
|
||||
} else {
|
||||
$elementNames[$name] = $id
|
||||
}
|
||||
}
|
||||
|
||||
# Recurse into ChildItems
|
||||
@@ -229,6 +237,10 @@ foreach ($attr in $attrNodes) {
|
||||
$attrName = $attr.GetAttribute("name")
|
||||
$attrId = $attr.GetAttribute("id")
|
||||
if ($attrName) {
|
||||
# Имена реквизитов уникальны среди реквизитов (отдельный неймспейс от элементов)
|
||||
if ($attrMap.ContainsKey($attrName)) {
|
||||
Report-Error "Duplicate attribute name '$attrName': id=${attrId} and id=$($attrMap[$attrName].GetAttribute('id'))"
|
||||
}
|
||||
$attrMap[$attrName] = $attr
|
||||
}
|
||||
if ($attrId -and $attrId -ne "") {
|
||||
@@ -241,6 +253,7 @@ foreach ($attr in $attrNodes) {
|
||||
|
||||
# Column IDs are a separate sub-pool per attribute — check uniqueness within parent
|
||||
$colIds = @{}
|
||||
$colNames = @{} # имена колонок уникальны в пределах своего реквизита
|
||||
foreach ($col in $attr.SelectNodes("f:Columns/f:Column", $nsMgr)) {
|
||||
$colId = $col.GetAttribute("id")
|
||||
$colName = $col.GetAttribute("name")
|
||||
@@ -251,6 +264,13 @@ foreach ($attr in $attrNodes) {
|
||||
$colIds[$colId] = $colName
|
||||
}
|
||||
}
|
||||
if ($colName) {
|
||||
if ($colNames.ContainsKey($colName)) {
|
||||
Report-Error "Duplicate column name '$colName' in '$attrName': id=${colId} and id=$($colNames[$colName])"
|
||||
} else {
|
||||
$colNames[$colName] = $colId
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -270,6 +290,10 @@ foreach ($cmd in $cmdNodes) {
|
||||
$cmdName = $cmd.GetAttribute("name")
|
||||
$cmdId = $cmd.GetAttribute("id")
|
||||
if ($cmdName) {
|
||||
# Имена команд уникальны среди команд (отдельный неймспейс)
|
||||
if ($cmdMap.ContainsKey($cmdName)) {
|
||||
Report-Error "Duplicate command name '$cmdName': id=${cmdId} and id=$($cmdMap[$cmdName].GetAttribute('id'))"
|
||||
}
|
||||
$cmdMap[$cmdName] = $cmd
|
||||
}
|
||||
if ($cmdId -and $cmdId -ne "") {
|
||||
@@ -290,6 +314,20 @@ if (-not $stopped) {
|
||||
}
|
||||
}
|
||||
|
||||
# --- Collect parameters (separate name pool, без id) ---
|
||||
|
||||
$paramNames = @{} # name -> $true (имена параметров уникальны среди параметров)
|
||||
foreach ($param in $root.SelectNodes("f:Parameters/f:Parameter", $nsMgr)) {
|
||||
$paramName = $param.GetAttribute("name")
|
||||
if ($paramName) {
|
||||
if ($paramNames.ContainsKey($paramName)) {
|
||||
Report-Error "Duplicate parameter name '$paramName'"
|
||||
} else {
|
||||
$paramNames[$paramName] = $true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# --- Check 4: Companion elements ---
|
||||
|
||||
# Define required companions per element type
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# form-validate v1.6 — Validate 1C managed form
|
||||
# form-validate v1.7 — Validate 1C managed form
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
@@ -182,7 +182,8 @@ def main():
|
||||
report_error("AutoCommandBar element missing")
|
||||
|
||||
# --- Collect all elements with IDs ---
|
||||
element_ids = {} # id -> name
|
||||
element_ids = {} # id -> name
|
||||
element_names = {} # name -> id (имена элементов уникальны в пределах формы)
|
||||
all_elements = [] # list of dicts {Name, Tag, Id, ParentName, Node}
|
||||
|
||||
def collect_elements(node, parent_name):
|
||||
@@ -211,6 +212,12 @@ def main():
|
||||
else:
|
||||
element_ids[eid] = name
|
||||
|
||||
# Имена элементов уникальны (требование 1С)
|
||||
if name in element_names:
|
||||
report_error(f"Duplicate element name '{name}': id={eid} and id={element_names[name]}")
|
||||
else:
|
||||
element_names[name] = eid
|
||||
|
||||
child_items = child.find(f"{{{F_NS}}}ChildItems")
|
||||
if child_items is not None:
|
||||
collect_elements(child_items, name)
|
||||
@@ -252,6 +259,9 @@ def main():
|
||||
attr_name = attr.get("name", "")
|
||||
attr_id = attr.get("id", "")
|
||||
if attr_name:
|
||||
# Имена реквизитов уникальны среди реквизитов (отдельный неймспейс от элементов)
|
||||
if attr_name in attr_map:
|
||||
report_error(f"Duplicate attribute name '{attr_name}': id={attr_id} and id={attr_map[attr_name].get('id', '')}")
|
||||
attr_map[attr_name] = attr
|
||||
if attr_id:
|
||||
if attr_id in attr_ids:
|
||||
@@ -261,6 +271,7 @@ def main():
|
||||
|
||||
# Column IDs uniqueness within parent
|
||||
col_ids = {}
|
||||
col_names = {} # имена колонок уникальны в пределах своего реквизита
|
||||
columns = attr.find(f"{{{F_NS}}}Columns")
|
||||
if columns is not None:
|
||||
for col in columns.findall(f"{{{F_NS}}}Column"):
|
||||
@@ -271,6 +282,11 @@ def main():
|
||||
report_error(f"Duplicate column id={col_id} in '{attr_name}': '{col_name}' and '{col_ids[col_id]}'")
|
||||
else:
|
||||
col_ids[col_id] = col_name
|
||||
if col_name:
|
||||
if col_name in col_names:
|
||||
report_error(f"Duplicate column name '{col_name}' in '{attr_name}': id={col_id} and id={col_names[col_name]}")
|
||||
else:
|
||||
col_names[col_name] = col_id
|
||||
|
||||
if not stopped:
|
||||
if attr_ids:
|
||||
@@ -289,6 +305,9 @@ def main():
|
||||
cmd_name = cmd.get("name", "")
|
||||
cmd_id = cmd.get("id", "")
|
||||
if cmd_name:
|
||||
# Имена команд уникальны среди команд (отдельный неймспейс)
|
||||
if cmd_name in cmd_map:
|
||||
report_error(f"Duplicate command name '{cmd_name}': id={cmd_id} and id={cmd_map[cmd_name].get('id', '')}")
|
||||
cmd_map[cmd_name] = cmd
|
||||
if cmd_id:
|
||||
if cmd_id in cmd_ids:
|
||||
@@ -300,6 +319,18 @@ def main():
|
||||
if cmd_ids:
|
||||
report_ok(f"Unique command IDs: {len(cmd_ids)} entries")
|
||||
|
||||
# --- Collect parameters (separate name pool, без id) ---
|
||||
param_names = {} # name -> True (имена параметров уникальны среди параметров)
|
||||
params_parent = root.find(f"{{{F_NS}}}Parameters")
|
||||
if params_parent is not None:
|
||||
for param in params_parent.findall(f"{{{F_NS}}}Parameter"):
|
||||
param_name = param.get("name", "")
|
||||
if param_name:
|
||||
if param_name in param_names:
|
||||
report_error(f"Duplicate parameter name '{param_name}'")
|
||||
else:
|
||||
param_names[param_name] = True
|
||||
|
||||
# --- Check 4: Companion elements ---
|
||||
companion_rules = {
|
||||
"InputField": ["ContextMenu", "ExtendedTooltip"],
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# help-add v1.4 — Add built-in help to 1C object
|
||||
# help-add v1.7 — Add built-in help to 1C object
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
@@ -13,6 +13,124 @@ $ErrorActionPreference = "Stop"
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
[Console]::InputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# --- Support guard (Ext/ParentConfigurations.bin) ---
|
||||
# See docs/1c-support-state-spec.md. Blocks edits of vendor objects "на замке" /
|
||||
# read-only configs unless allowed. Trigger = bin present; reaction from
|
||||
# .v8-project.json editingAllowedCheck (deny|warn|off, default deny). Never
|
||||
# throws — guard errors degrade to allow.
|
||||
function Get-RootUuid([string]$xmlPath) {
|
||||
if (-not (Test-Path $xmlPath)) { return $null }
|
||||
try {
|
||||
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
|
||||
$el = $mx.DocumentElement.FirstChild
|
||||
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
|
||||
if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } }
|
||||
} catch {}
|
||||
return $null
|
||||
}
|
||||
function Find-V8Project([string]$startDir) {
|
||||
$d = $startDir
|
||||
for ($i = 0; $i -lt 20 -and $d; $i++) {
|
||||
$pj = Join-Path $d ".v8-project.json"
|
||||
if (Test-Path $pj) { return $pj }
|
||||
$parent = [System.IO.Path]::GetDirectoryName($d)
|
||||
if ($parent -eq $d) { break }
|
||||
$d = $parent
|
||||
}
|
||||
return $null
|
||||
}
|
||||
function Get-EditMode([string]$cfgDir) {
|
||||
try {
|
||||
$pj = Find-V8Project (Get-Location).Path
|
||||
if (-not $pj) { $pj = Find-V8Project $cfgDir }
|
||||
if (-not $pj) { return 'deny' }
|
||||
$proj = Get-Content -Raw $pj | ConvertFrom-Json
|
||||
$cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/')
|
||||
if ($proj.databases) {
|
||||
foreach ($db in $proj.databases) {
|
||||
if ($db.configSrc) {
|
||||
$src = [System.IO.Path]::GetFullPath($db.configSrc).TrimEnd('\', '/')
|
||||
if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) {
|
||||
if ($db.editingAllowedCheck) { return $db.editingAllowedCheck }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($proj.editingAllowedCheck) { return $proj.editingAllowedCheck }
|
||||
return 'deny'
|
||||
} catch { return 'deny' }
|
||||
}
|
||||
function Assert-EditAllowed([string]$targetPath, [string]$require) {
|
||||
try {
|
||||
$rp = $targetPath
|
||||
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
|
||||
$elemUuid = Get-RootUuid $rp
|
||||
$cfgDir = $null; $binPath = $null
|
||||
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
|
||||
for ($i = 0; $i -lt 12 -and $d; $i++) {
|
||||
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
|
||||
if (-not $cfgDir) {
|
||||
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
|
||||
if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $cfgDir = $d; $binPath = $cand }
|
||||
}
|
||||
if ($elemUuid -and $cfgDir) { break }
|
||||
$parent = [System.IO.Path]::GetDirectoryName($d)
|
||||
if ($parent -eq $d) { break }
|
||||
$d = $parent
|
||||
}
|
||||
# New object (no element file): fall back to config root uuid.
|
||||
if (-not $elemUuid -and $cfgDir) { $elemUuid = Get-RootUuid (Join-Path $cfgDir "Configuration.xml") }
|
||||
if (-not $binPath -or -not (Test-Path $binPath)) { return }
|
||||
$bytes = [System.IO.File]::ReadAllBytes($binPath)
|
||||
if ($bytes.Length -le 32) { return }
|
||||
$start = 0
|
||||
if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 }
|
||||
$text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start)
|
||||
$hm = [regex]::Match($text, '^\{6,(\d+),(\d+),')
|
||||
if (-not $hm.Success) { return }
|
||||
$G = [int]$hm.Groups[1].Value
|
||||
$K = [int]$hm.Groups[2].Value
|
||||
if ($K -eq 0) { return }
|
||||
$best = $null
|
||||
if ($elemUuid) {
|
||||
$u = [regex]::Escape($elemUuid.ToLower())
|
||||
foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) {
|
||||
$f1 = [int]$m.Groups[1].Value
|
||||
if ($null -eq $best -or $f1 -lt $best) { $best = $f1 }
|
||||
}
|
||||
}
|
||||
$blocked = $false; $code = ""; $reason = ""
|
||||
if ($G -eq 1) { $blocked = $true; $code = "capability-off"; $reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)" }
|
||||
elseif ($require -eq 'removed') {
|
||||
if ($null -ne $best -and $best -ne 2) { $blocked = $true; $code = "not-removed"; $reason = "объект не снят с поддержки — удаление сломает обновления" }
|
||||
}
|
||||
else {
|
||||
if ($null -ne $best -and $best -eq 0) { $blocked = $true; $code = "locked"; $reason = "объект на замке — редактирование сломает обновления" }
|
||||
}
|
||||
if (-not $blocked) { return }
|
||||
$mode = Get-EditMode $cfgDir
|
||||
if ($mode -eq 'off') { return }
|
||||
# Use Console.Error (not Write-Error) — under ErrorActionPreference=Stop the
|
||||
# latter throws and would be swallowed by this function's own catch.
|
||||
if ($mode -eq 'warn') { [Console]::Error.WriteLine("[support-guard] ПРЕДУПРЕЖДЕНИЕ: $reason. Цель: $rp"); return }
|
||||
$head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления."
|
||||
$cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются."
|
||||
$offNote = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json."
|
||||
if ($code -eq "capability-off") {
|
||||
$state = "Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «$rp» редактировать нельзя."
|
||||
$fix = "Либо снять защиту явно (навык support-edit, два шага):`n 1. support-edit -Path ""$cfgDir"" -Capability on — включить возможность изменения (объекты пока остаются на замке);`n 2. support-edit -Path ""$rp"" -Set editable — открыть этот объект для редактирования.`n Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора."
|
||||
} elseif ($code -eq "not-removed") {
|
||||
$state = "Состояние: объект «$rp» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора."
|
||||
$fix = "Либо сначала снять объект с поддержки, затем удалять:`n support-edit -Path ""$rp"" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно."
|
||||
} else {
|
||||
$state = "Состояние: объект «$rp» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)."
|
||||
$fix = "Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):`n support-edit -Path ""$rp"" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);`n support-edit -Path ""$rp"" -Set off-support — снять с поддержки: обновления по объекту больше не приходят."
|
||||
}
|
||||
[Console]::Error.WriteLine("$head`n$state`n$cfe`n$fix`n$offNote")
|
||||
exit 1
|
||||
} catch { return }
|
||||
}
|
||||
|
||||
# --- Detect format version ---
|
||||
|
||||
function Detect-FormatVersion([string]$dir) {
|
||||
@@ -20,7 +138,8 @@ function Detect-FormatVersion([string]$dir) {
|
||||
while ($d) {
|
||||
$cfgPath = Join-Path $d "Configuration.xml"
|
||||
if (Test-Path $cfgPath) {
|
||||
$head = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8).Substring(0, [Math]::Min(2000, (Get-Item $cfgPath).Length))
|
||||
$content = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8)
|
||||
$head = $content.Substring(0, [Math]::Min(2000, $content.Length))
|
||||
if ($head -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
|
||||
}
|
||||
$parent = Split-Path $d -Parent
|
||||
@@ -48,6 +167,8 @@ if (Test-Path $helpXmlPath) {
|
||||
exit 1
|
||||
}
|
||||
|
||||
Assert-EditAllowed $objectDir 'editable'
|
||||
|
||||
# --- Кодировка ---
|
||||
|
||||
$encBom = New-Object System.Text.UTF8Encoding($true)
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
#!/usr/bin/env python3
|
||||
# add-help v1.4 — Add built-in help to 1C object
|
||||
# add-help v1.7 — Add built-in help to 1C object
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
@@ -12,6 +13,165 @@ from lxml import etree
|
||||
NSMAP = {"md": "http://v8.1c.ru/8.3/MDClasses"}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md
|
||||
# Blocks edits of vendor objects "на замке" / read-only configs. Trigger = bin
|
||||
# present; reaction from .v8-project.json editingAllowedCheck (deny|warn|off,
|
||||
# default deny). Never throws (except sys.exit on deny) — errors degrade to allow.
|
||||
# ============================================================
|
||||
|
||||
def _sg_root_uuid(xml_path):
|
||||
if not os.path.isfile(xml_path):
|
||||
return None
|
||||
try:
|
||||
mx = etree.parse(xml_path).getroot()
|
||||
for child in mx:
|
||||
if isinstance(child.tag, str) and child.get("uuid"):
|
||||
return child.get("uuid")
|
||||
except Exception:
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def _sg_find_v8project(start_dir):
|
||||
d = start_dir
|
||||
for _ in range(20):
|
||||
if not d:
|
||||
break
|
||||
pj = os.path.join(d, ".v8-project.json")
|
||||
if os.path.isfile(pj):
|
||||
return pj
|
||||
parent = os.path.dirname(d)
|
||||
if parent == d:
|
||||
break
|
||||
d = parent
|
||||
return None
|
||||
|
||||
|
||||
def _sg_get_edit_mode(cfg_dir):
|
||||
try:
|
||||
pj = _sg_find_v8project(os.getcwd()) or _sg_find_v8project(cfg_dir)
|
||||
if not pj:
|
||||
return "deny"
|
||||
proj = json.loads(open(pj, encoding="utf-8-sig").read())
|
||||
cfg_full = os.path.normcase(os.path.abspath(cfg_dir)).rstrip("\\/")
|
||||
for db in proj.get("databases", []):
|
||||
src = db.get("configSrc")
|
||||
if src:
|
||||
src_full = os.path.normcase(os.path.abspath(src)).rstrip("\\/")
|
||||
if cfg_full == src_full or cfg_full.startswith(src_full + os.sep):
|
||||
if db.get("editingAllowedCheck"):
|
||||
return db["editingAllowedCheck"]
|
||||
if proj.get("editingAllowedCheck"):
|
||||
return proj["editingAllowedCheck"]
|
||||
return "deny"
|
||||
except Exception:
|
||||
return "deny"
|
||||
|
||||
|
||||
def assert_edit_allowed(target_path, require):
|
||||
try:
|
||||
rp = os.path.abspath(target_path)
|
||||
elem_uuid = _sg_root_uuid(rp)
|
||||
cfg_dir = None
|
||||
bin_path = None
|
||||
d = rp if os.path.isdir(rp) else os.path.dirname(rp)
|
||||
for _ in range(12):
|
||||
if not d:
|
||||
break
|
||||
if not elem_uuid:
|
||||
elem_uuid = _sg_root_uuid(d + ".xml")
|
||||
if not cfg_dir:
|
||||
cand = os.path.join(d, "Ext", "ParentConfigurations.bin")
|
||||
if os.path.exists(cand) or os.path.exists(os.path.join(d, "Configuration.xml")):
|
||||
cfg_dir = d
|
||||
bin_path = cand
|
||||
if elem_uuid and cfg_dir:
|
||||
break
|
||||
parent = os.path.dirname(d)
|
||||
if parent == d:
|
||||
break
|
||||
d = parent
|
||||
if not elem_uuid and cfg_dir:
|
||||
elem_uuid = _sg_root_uuid(os.path.join(cfg_dir, "Configuration.xml"))
|
||||
if not bin_path or not os.path.exists(bin_path):
|
||||
return
|
||||
data = open(bin_path, "rb").read()
|
||||
if len(data) <= 32:
|
||||
return
|
||||
if data[:3] == b"\xef\xbb\xbf":
|
||||
data = data[3:]
|
||||
text = data.decode("utf-8", "replace")
|
||||
h = re.match(r"\{6,(\d+),(\d+),", text)
|
||||
if not h:
|
||||
return
|
||||
g = int(h.group(1))
|
||||
k = int(h.group(2))
|
||||
if k == 0:
|
||||
return
|
||||
best = None
|
||||
if elem_uuid:
|
||||
for m in re.finditer(r"([0-2]),0," + re.escape(elem_uuid.lower()), text):
|
||||
f1 = int(m.group(1))
|
||||
if best is None or f1 < best:
|
||||
best = f1
|
||||
blocked = False
|
||||
code = ""
|
||||
reason = ""
|
||||
if g == 1:
|
||||
blocked = True
|
||||
code = "capability-off"
|
||||
reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)"
|
||||
elif require == "removed":
|
||||
if best is not None and best != 2:
|
||||
blocked = True
|
||||
code = "not-removed"
|
||||
reason = "объект не снят с поддержки — удаление сломает обновления"
|
||||
else:
|
||||
if best is not None and best == 0:
|
||||
blocked = True
|
||||
code = "locked"
|
||||
reason = "объект на замке — редактирование сломает обновления"
|
||||
if not blocked:
|
||||
return
|
||||
mode = _sg_get_edit_mode(cfg_dir)
|
||||
if mode == "off":
|
||||
return
|
||||
if mode == "warn":
|
||||
sys.stderr.write(f"[support-guard] ПРЕДУПРЕЖДЕНИЕ: {reason}. Цель: {rp}\n")
|
||||
return
|
||||
head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления."
|
||||
cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются."
|
||||
off_note = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json."
|
||||
if code == "capability-off":
|
||||
state = f"Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «{rp}» редактировать нельзя."
|
||||
fix = (
|
||||
"Либо снять защиту явно (навык support-edit, два шага):\n"
|
||||
f' 1. support-edit -Path "{cfg_dir}" -Capability on — включить возможность изменения (объекты пока остаются на замке);\n'
|
||||
f' 2. support-edit -Path "{rp}" -Set editable — открыть этот объект для редактирования.\n'
|
||||
" Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора."
|
||||
)
|
||||
elif code == "not-removed":
|
||||
state = f"Состояние: объект «{rp}» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора."
|
||||
fix = (
|
||||
"Либо сначала снять объект с поддержки, затем удалять:\n"
|
||||
f' support-edit -Path "{rp}" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно.'
|
||||
)
|
||||
else:
|
||||
state = f"Состояние: объект «{rp}» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)."
|
||||
fix = (
|
||||
"Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):\n"
|
||||
f' support-edit -Path "{rp}" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);\n'
|
||||
f' support-edit -Path "{rp}" -Set off-support — снять с поддержки: обновления по объекту больше не приходят.'
|
||||
)
|
||||
sys.stderr.write(head + "\n" + state + "\n" + cfe + "\n" + fix + "\n" + off_note + "\n")
|
||||
sys.exit(1)
|
||||
except SystemExit:
|
||||
raise
|
||||
except Exception:
|
||||
return
|
||||
|
||||
|
||||
def detect_format_version(d):
|
||||
while d:
|
||||
cfg_path = os.path.join(d, "Configuration.xml")
|
||||
@@ -74,6 +234,8 @@ def main():
|
||||
print(f"Справка уже существует: {help_xml_path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
assert_edit_allowed(object_dir, "editable")
|
||||
|
||||
# --- 1. Help.xml ---
|
||||
|
||||
help_xml = (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# interface-edit v1.3 — Edit 1C CommandInterface.xml
|
||||
# interface-edit v1.6 — Edit 1C CommandInterface.xml
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory)][Alias('Path')][string]$CIPath,
|
||||
@@ -23,6 +23,127 @@ if (-not [System.IO.Path]::IsPathRooted($CIPath)) {
|
||||
}
|
||||
$resolvedPath = $CIPath
|
||||
|
||||
# --- Support guard (Ext/ParentConfigurations.bin) ---
|
||||
# See docs/1c-support-state-spec.md. Blocks edits of vendor objects "на замке" /
|
||||
# read-only configs unless allowed. Trigger = bin present; reaction from
|
||||
# .v8-project.json editingAllowedCheck (deny|warn|off, default deny). Never
|
||||
# throws — guard errors degrade to allow. Walk-up from Subsystems/X/Ext/
|
||||
# CommandInterface.xml reaches Subsystems/X.xml (owning subsystem uuid).
|
||||
function Get-RootUuid([string]$xmlPath) {
|
||||
if (-not (Test-Path $xmlPath)) { return $null }
|
||||
try {
|
||||
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
|
||||
$el = $mx.DocumentElement.FirstChild
|
||||
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
|
||||
if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } }
|
||||
} catch {}
|
||||
return $null
|
||||
}
|
||||
function Find-V8Project([string]$startDir) {
|
||||
$d = $startDir
|
||||
for ($i = 0; $i -lt 20 -and $d; $i++) {
|
||||
$pj = Join-Path $d ".v8-project.json"
|
||||
if (Test-Path $pj) { return $pj }
|
||||
$parent = [System.IO.Path]::GetDirectoryName($d)
|
||||
if ($parent -eq $d) { break }
|
||||
$d = $parent
|
||||
}
|
||||
return $null
|
||||
}
|
||||
function Get-EditMode([string]$cfgDir) {
|
||||
try {
|
||||
$pj = Find-V8Project (Get-Location).Path
|
||||
if (-not $pj) { $pj = Find-V8Project $cfgDir }
|
||||
if (-not $pj) { return 'deny' }
|
||||
$proj = Get-Content -Raw $pj | ConvertFrom-Json
|
||||
$cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/')
|
||||
if ($proj.databases) {
|
||||
foreach ($db in $proj.databases) {
|
||||
if ($db.configSrc) {
|
||||
$src = [System.IO.Path]::GetFullPath($db.configSrc).TrimEnd('\', '/')
|
||||
if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) {
|
||||
if ($db.editingAllowedCheck) { return $db.editingAllowedCheck }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($proj.editingAllowedCheck) { return $proj.editingAllowedCheck }
|
||||
return 'deny'
|
||||
} catch { return 'deny' }
|
||||
}
|
||||
function Assert-EditAllowed([string]$targetPath, [string]$require) {
|
||||
try {
|
||||
$rp = $targetPath
|
||||
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
|
||||
$elemUuid = Get-RootUuid $rp
|
||||
$cfgDir = $null; $binPath = $null
|
||||
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
|
||||
for ($i = 0; $i -lt 12 -and $d; $i++) {
|
||||
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
|
||||
if (-not $cfgDir) {
|
||||
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
|
||||
if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $cfgDir = $d; $binPath = $cand }
|
||||
}
|
||||
if ($elemUuid -and $cfgDir) { break }
|
||||
$parent = [System.IO.Path]::GetDirectoryName($d)
|
||||
if ($parent -eq $d) { break }
|
||||
$d = $parent
|
||||
}
|
||||
# New object (no element file): fall back to config root uuid.
|
||||
if (-not $elemUuid -and $cfgDir) { $elemUuid = Get-RootUuid (Join-Path $cfgDir "Configuration.xml") }
|
||||
if (-not $binPath -or -not (Test-Path $binPath)) { return }
|
||||
$bytes = [System.IO.File]::ReadAllBytes($binPath)
|
||||
if ($bytes.Length -le 32) { return }
|
||||
$start = 0
|
||||
if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 }
|
||||
$text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start)
|
||||
$hm = [regex]::Match($text, '^\{6,(\d+),(\d+),')
|
||||
if (-not $hm.Success) { return }
|
||||
$G = [int]$hm.Groups[1].Value
|
||||
$K = [int]$hm.Groups[2].Value
|
||||
if ($K -eq 0) { return }
|
||||
$best = $null
|
||||
if ($elemUuid) {
|
||||
$u = [regex]::Escape($elemUuid.ToLower())
|
||||
foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) {
|
||||
$f1 = [int]$m.Groups[1].Value
|
||||
if ($null -eq $best -or $f1 -lt $best) { $best = $f1 }
|
||||
}
|
||||
}
|
||||
$blocked = $false; $code = ""; $reason = ""
|
||||
if ($G -eq 1) { $blocked = $true; $code = "capability-off"; $reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)" }
|
||||
elseif ($require -eq 'removed') {
|
||||
if ($null -ne $best -and $best -ne 2) { $blocked = $true; $code = "not-removed"; $reason = "объект не снят с поддержки — удаление сломает обновления" }
|
||||
}
|
||||
else {
|
||||
if ($null -ne $best -and $best -eq 0) { $blocked = $true; $code = "locked"; $reason = "объект на замке — редактирование сломает обновления" }
|
||||
}
|
||||
if (-not $blocked) { return }
|
||||
$mode = Get-EditMode $cfgDir
|
||||
if ($mode -eq 'off') { return }
|
||||
# Use Console.Error (not Write-Error) — under ErrorActionPreference=Stop the
|
||||
# latter throws and would be swallowed by this function's own catch.
|
||||
if ($mode -eq 'warn') { [Console]::Error.WriteLine("[support-guard] ПРЕДУПРЕЖДЕНИЕ: $reason. Цель: $rp"); return }
|
||||
$head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления."
|
||||
$cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются."
|
||||
$offNote = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json."
|
||||
if ($code -eq "capability-off") {
|
||||
$state = "Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «$rp» редактировать нельзя."
|
||||
$fix = "Либо снять защиту явно (навык support-edit, два шага):`n 1. support-edit -Path ""$cfgDir"" -Capability on — включить возможность изменения (объекты пока остаются на замке);`n 2. support-edit -Path ""$rp"" -Set editable — открыть этот объект для редактирования.`n Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора."
|
||||
} elseif ($code -eq "not-removed") {
|
||||
$state = "Состояние: объект «$rp» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора."
|
||||
$fix = "Либо сначала снять объект с поддержки, затем удалять:`n support-edit -Path ""$rp"" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно."
|
||||
} else {
|
||||
$state = "Состояние: объект «$rp» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)."
|
||||
$fix = "Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):`n support-edit -Path ""$rp"" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);`n support-edit -Path ""$rp"" -Set off-support — снять с поддержки: обновления по объекту больше не приходят."
|
||||
}
|
||||
[Console]::Error.WriteLine("$head`n$state`n$cfe`n$fix`n$offNote")
|
||||
exit 1
|
||||
} catch { return }
|
||||
}
|
||||
|
||||
Assert-EditAllowed $CIPath 'editable'
|
||||
|
||||
# --- Detect format version ---
|
||||
|
||||
function Detect-FormatVersion([string]$dir) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# interface-edit v1.3 — Edit 1C CommandInterface.xml
|
||||
# interface-edit v1.6 — Edit 1C CommandInterface.xml
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
@@ -10,6 +10,166 @@ import subprocess
|
||||
import sys
|
||||
from lxml import etree
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md
|
||||
# Blocks edits of vendor objects "на замке" / read-only configs. Trigger = bin
|
||||
# present; reaction from .v8-project.json editingAllowedCheck (deny|warn|off,
|
||||
# default deny). Never throws (except sys.exit on deny) — errors degrade to allow.
|
||||
# ============================================================
|
||||
|
||||
def _sg_root_uuid(xml_path):
|
||||
if not os.path.isfile(xml_path):
|
||||
return None
|
||||
try:
|
||||
mx = etree.parse(xml_path).getroot()
|
||||
for child in mx:
|
||||
if isinstance(child.tag, str) and child.get("uuid"):
|
||||
return child.get("uuid")
|
||||
except Exception:
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def _sg_find_v8project(start_dir):
|
||||
d = start_dir
|
||||
for _ in range(20):
|
||||
if not d:
|
||||
break
|
||||
pj = os.path.join(d, ".v8-project.json")
|
||||
if os.path.isfile(pj):
|
||||
return pj
|
||||
parent = os.path.dirname(d)
|
||||
if parent == d:
|
||||
break
|
||||
d = parent
|
||||
return None
|
||||
|
||||
|
||||
def _sg_get_edit_mode(cfg_dir):
|
||||
try:
|
||||
pj = _sg_find_v8project(os.getcwd()) or _sg_find_v8project(cfg_dir)
|
||||
if not pj:
|
||||
return "deny"
|
||||
proj = json.loads(open(pj, encoding="utf-8-sig").read())
|
||||
cfg_full = os.path.normcase(os.path.abspath(cfg_dir)).rstrip("\\/")
|
||||
for db in proj.get("databases", []):
|
||||
src = db.get("configSrc")
|
||||
if src:
|
||||
src_full = os.path.normcase(os.path.abspath(src)).rstrip("\\/")
|
||||
if cfg_full == src_full or cfg_full.startswith(src_full + os.sep):
|
||||
if db.get("editingAllowedCheck"):
|
||||
return db["editingAllowedCheck"]
|
||||
if proj.get("editingAllowedCheck"):
|
||||
return proj["editingAllowedCheck"]
|
||||
return "deny"
|
||||
except Exception:
|
||||
return "deny"
|
||||
|
||||
|
||||
def assert_edit_allowed(target_path, require):
|
||||
try:
|
||||
rp = os.path.abspath(target_path)
|
||||
elem_uuid = _sg_root_uuid(rp)
|
||||
cfg_dir = None
|
||||
bin_path = None
|
||||
d = rp if os.path.isdir(rp) else os.path.dirname(rp)
|
||||
for _ in range(12):
|
||||
if not d:
|
||||
break
|
||||
if not elem_uuid:
|
||||
elem_uuid = _sg_root_uuid(d + ".xml")
|
||||
if not cfg_dir:
|
||||
cand = os.path.join(d, "Ext", "ParentConfigurations.bin")
|
||||
if os.path.exists(cand) or os.path.exists(os.path.join(d, "Configuration.xml")):
|
||||
cfg_dir = d
|
||||
bin_path = cand
|
||||
if elem_uuid and cfg_dir:
|
||||
break
|
||||
parent = os.path.dirname(d)
|
||||
if parent == d:
|
||||
break
|
||||
d = parent
|
||||
if not elem_uuid and cfg_dir:
|
||||
elem_uuid = _sg_root_uuid(os.path.join(cfg_dir, "Configuration.xml"))
|
||||
if not bin_path or not os.path.exists(bin_path):
|
||||
return
|
||||
data = open(bin_path, "rb").read()
|
||||
if len(data) <= 32:
|
||||
return
|
||||
if data[:3] == b"\xef\xbb\xbf":
|
||||
data = data[3:]
|
||||
text = data.decode("utf-8", "replace")
|
||||
h = re.match(r"\{6,(\d+),(\d+),", text)
|
||||
if not h:
|
||||
return
|
||||
g = int(h.group(1))
|
||||
k = int(h.group(2))
|
||||
if k == 0:
|
||||
return
|
||||
best = None
|
||||
if elem_uuid:
|
||||
for m in re.finditer(r"([0-2]),0," + re.escape(elem_uuid.lower()), text):
|
||||
f1 = int(m.group(1))
|
||||
if best is None or f1 < best:
|
||||
best = f1
|
||||
blocked = False
|
||||
code = ""
|
||||
reason = ""
|
||||
if g == 1:
|
||||
blocked = True
|
||||
code = "capability-off"
|
||||
reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)"
|
||||
elif require == "removed":
|
||||
if best is not None and best != 2:
|
||||
blocked = True
|
||||
code = "not-removed"
|
||||
reason = "объект не снят с поддержки — удаление сломает обновления"
|
||||
else:
|
||||
if best is not None and best == 0:
|
||||
blocked = True
|
||||
code = "locked"
|
||||
reason = "объект на замке — редактирование сломает обновления"
|
||||
if not blocked:
|
||||
return
|
||||
mode = _sg_get_edit_mode(cfg_dir)
|
||||
if mode == "off":
|
||||
return
|
||||
if mode == "warn":
|
||||
sys.stderr.write(f"[support-guard] ПРЕДУПРЕЖДЕНИЕ: {reason}. Цель: {rp}\n")
|
||||
return
|
||||
head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления."
|
||||
cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются."
|
||||
off_note = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json."
|
||||
if code == "capability-off":
|
||||
state = f"Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «{rp}» редактировать нельзя."
|
||||
fix = (
|
||||
"Либо снять защиту явно (навык support-edit, два шага):\n"
|
||||
f' 1. support-edit -Path "{cfg_dir}" -Capability on — включить возможность изменения (объекты пока остаются на замке);\n'
|
||||
f' 2. support-edit -Path "{rp}" -Set editable — открыть этот объект для редактирования.\n'
|
||||
" Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора."
|
||||
)
|
||||
elif code == "not-removed":
|
||||
state = f"Состояние: объект «{rp}» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора."
|
||||
fix = (
|
||||
"Либо сначала снять объект с поддержки, затем удалять:\n"
|
||||
f' support-edit -Path "{rp}" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно.'
|
||||
)
|
||||
else:
|
||||
state = f"Состояние: объект «{rp}» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)."
|
||||
fix = (
|
||||
"Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):\n"
|
||||
f' support-edit -Path "{rp}" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);\n'
|
||||
f' support-edit -Path "{rp}" -Set off-support — снять с поддержки: обновления по объекту больше не приходят.'
|
||||
)
|
||||
sys.stderr.write(head + "\n" + state + "\n" + cfe + "\n" + fix + "\n" + off_note + "\n")
|
||||
sys.exit(1)
|
||||
except SystemExit:
|
||||
raise
|
||||
except Exception:
|
||||
return
|
||||
|
||||
|
||||
def detect_format_version(d):
|
||||
while d:
|
||||
cfg_path = os.path.join(d, "Configuration.xml")
|
||||
@@ -208,6 +368,8 @@ def main():
|
||||
ci_path = os.path.join(os.getcwd(), ci_path)
|
||||
resolved_path = ci_path
|
||||
|
||||
assert_edit_allowed(ci_path, "editable")
|
||||
|
||||
# --- Create if missing ---
|
||||
if not os.path.isfile(ci_path):
|
||||
if args.CreateIfMissing:
|
||||
@@ -504,7 +666,7 @@ def main():
|
||||
if os.path.isfile(validate_script):
|
||||
print()
|
||||
print("--- Running interface-validate ---")
|
||||
subprocess.run([sys.executable, validate_script, "-CIPath", "-Path", resolved_path])
|
||||
subprocess.run([sys.executable, validate_script, "-CIPath", resolved_path])
|
||||
|
||||
# --- Summary ---
|
||||
print()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# meta-compile v1.12 — Compile 1C metadata object from JSON
|
||||
# meta-compile v1.14 — Compile 1C metadata object from JSON
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
@@ -21,6 +21,126 @@ if (-not (Test-Path $JsonPath)) {
|
||||
$json = Get-Content -Raw -Encoding UTF8 $JsonPath
|
||||
$def = $json | ConvertFrom-Json
|
||||
|
||||
# --- Support guard (Ext/ParentConfigurations.bin) ---
|
||||
# See docs/1c-support-state-spec.md. Blocks edits of vendor objects "на замке" /
|
||||
# read-only configs unless allowed. Trigger = bin present; reaction from
|
||||
# .v8-project.json editingAllowedCheck (deny|warn|off, default deny). Never
|
||||
# throws — guard errors degrade to allow.
|
||||
function Get-RootUuid([string]$xmlPath) {
|
||||
if (-not (Test-Path $xmlPath)) { return $null }
|
||||
try {
|
||||
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
|
||||
$el = $mx.DocumentElement.FirstChild
|
||||
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
|
||||
if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } }
|
||||
} catch {}
|
||||
return $null
|
||||
}
|
||||
function Find-V8Project([string]$startDir) {
|
||||
$d = $startDir
|
||||
for ($i = 0; $i -lt 20 -and $d; $i++) {
|
||||
$pj = Join-Path $d ".v8-project.json"
|
||||
if (Test-Path $pj) { return $pj }
|
||||
$parent = [System.IO.Path]::GetDirectoryName($d)
|
||||
if ($parent -eq $d) { break }
|
||||
$d = $parent
|
||||
}
|
||||
return $null
|
||||
}
|
||||
function Get-EditMode([string]$cfgDir) {
|
||||
try {
|
||||
$pj = Find-V8Project (Get-Location).Path
|
||||
if (-not $pj) { $pj = Find-V8Project $cfgDir }
|
||||
if (-not $pj) { return 'deny' }
|
||||
$proj = Get-Content -Raw $pj | ConvertFrom-Json
|
||||
$cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/')
|
||||
if ($proj.databases) {
|
||||
foreach ($db in $proj.databases) {
|
||||
if ($db.configSrc) {
|
||||
$src = [System.IO.Path]::GetFullPath($db.configSrc).TrimEnd('\', '/')
|
||||
if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) {
|
||||
if ($db.editingAllowedCheck) { return $db.editingAllowedCheck }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($proj.editingAllowedCheck) { return $proj.editingAllowedCheck }
|
||||
return 'deny'
|
||||
} catch { return 'deny' }
|
||||
}
|
||||
function Assert-EditAllowed([string]$targetPath, [string]$require) {
|
||||
try {
|
||||
$rp = $targetPath
|
||||
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
|
||||
$elemUuid = Get-RootUuid $rp
|
||||
$cfgDir = $null; $binPath = $null
|
||||
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
|
||||
for ($i = 0; $i -lt 12 -and $d; $i++) {
|
||||
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
|
||||
if (-not $cfgDir) {
|
||||
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
|
||||
if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $cfgDir = $d; $binPath = $cand }
|
||||
}
|
||||
if ($elemUuid -and $cfgDir) { break }
|
||||
$parent = [System.IO.Path]::GetDirectoryName($d)
|
||||
if ($parent -eq $d) { break }
|
||||
$d = $parent
|
||||
}
|
||||
# New object (no element file): fall back to config root uuid.
|
||||
if (-not $elemUuid -and $cfgDir) { $elemUuid = Get-RootUuid (Join-Path $cfgDir "Configuration.xml") }
|
||||
if (-not $binPath -or -not (Test-Path $binPath)) { return }
|
||||
$bytes = [System.IO.File]::ReadAllBytes($binPath)
|
||||
if ($bytes.Length -le 32) { return }
|
||||
$start = 0
|
||||
if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 }
|
||||
$text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start)
|
||||
$hm = [regex]::Match($text, '^\{6,(\d+),(\d+),')
|
||||
if (-not $hm.Success) { return }
|
||||
$G = [int]$hm.Groups[1].Value
|
||||
$K = [int]$hm.Groups[2].Value
|
||||
if ($K -eq 0) { return }
|
||||
$best = $null
|
||||
if ($elemUuid) {
|
||||
$u = [regex]::Escape($elemUuid.ToLower())
|
||||
foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) {
|
||||
$f1 = [int]$m.Groups[1].Value
|
||||
if ($null -eq $best -or $f1 -lt $best) { $best = $f1 }
|
||||
}
|
||||
}
|
||||
$blocked = $false; $code = ""; $reason = ""
|
||||
if ($G -eq 1) { $blocked = $true; $code = "capability-off"; $reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)" }
|
||||
elseif ($require -eq 'removed') {
|
||||
if ($null -ne $best -and $best -ne 2) { $blocked = $true; $code = "not-removed"; $reason = "объект не снят с поддержки — удаление сломает обновления" }
|
||||
}
|
||||
else {
|
||||
if ($null -ne $best -and $best -eq 0) { $blocked = $true; $code = "locked"; $reason = "объект на замке — редактирование сломает обновления" }
|
||||
}
|
||||
if (-not $blocked) { return }
|
||||
$mode = Get-EditMode $cfgDir
|
||||
if ($mode -eq 'off') { return }
|
||||
# Use Console.Error (not Write-Error) — under ErrorActionPreference=Stop the
|
||||
# latter throws and would be swallowed by this function's own catch.
|
||||
if ($mode -eq 'warn') { [Console]::Error.WriteLine("[support-guard] ПРЕДУПРЕЖДЕНИЕ: $reason. Цель: $rp"); return }
|
||||
$head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления."
|
||||
$cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются."
|
||||
$offNote = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json."
|
||||
if ($code -eq "capability-off") {
|
||||
$state = "Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «$rp» редактировать нельзя."
|
||||
$fix = "Либо снять защиту явно (навык support-edit, два шага):`n 1. support-edit -Path ""$cfgDir"" -Capability on — включить возможность изменения (объекты пока остаются на замке);`n 2. support-edit -Path ""$rp"" -Set editable — открыть этот объект для редактирования.`n Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора."
|
||||
} elseif ($code -eq "not-removed") {
|
||||
$state = "Состояние: объект «$rp» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора."
|
||||
$fix = "Либо сначала снять объект с поддержки, затем удалять:`n support-edit -Path ""$rp"" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно."
|
||||
} else {
|
||||
$state = "Состояние: объект «$rp» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)."
|
||||
$fix = "Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):`n support-edit -Path ""$rp"" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);`n support-edit -Path ""$rp"" -Set off-support — снять с поддержки: обновления по объекту больше не приходят."
|
||||
}
|
||||
[Console]::Error.WriteLine("$head`n$state`n$cfe`n$fix`n$offNote")
|
||||
exit 1
|
||||
} catch { return }
|
||||
}
|
||||
|
||||
Assert-EditAllowed $OutputDir 'editable'
|
||||
|
||||
# --- Batch mode: JSON array of objects ---
|
||||
if ($def -is [array] -or ($null -ne $def -and $def.GetType().BaseType.Name -eq 'Array')) {
|
||||
$batchOk = 0
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# meta-compile v1.12 — Compile 1C metadata object from JSON
|
||||
# meta-compile v1.14 — Compile 1C metadata object from JSON
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
@@ -11,10 +11,169 @@ import sys
|
||||
import tempfile
|
||||
import uuid
|
||||
import xml.etree.ElementTree as ET
|
||||
from lxml import etree
|
||||
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
sys.stderr.reconfigure(encoding="utf-8")
|
||||
|
||||
# ============================================================
|
||||
# Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md
|
||||
# Blocks edits of vendor objects "на замке" / read-only configs. Trigger = bin
|
||||
# present; reaction from .v8-project.json editingAllowedCheck (deny|warn|off,
|
||||
# default deny). Never throws (except sys.exit on deny) — errors degrade to allow.
|
||||
# ============================================================
|
||||
|
||||
def _sg_root_uuid(xml_path):
|
||||
if not os.path.isfile(xml_path):
|
||||
return None
|
||||
try:
|
||||
mx = etree.parse(xml_path).getroot()
|
||||
for child in mx:
|
||||
if isinstance(child.tag, str) and child.get("uuid"):
|
||||
return child.get("uuid")
|
||||
except Exception:
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def _sg_find_v8project(start_dir):
|
||||
d = start_dir
|
||||
for _ in range(20):
|
||||
if not d:
|
||||
break
|
||||
pj = os.path.join(d, ".v8-project.json")
|
||||
if os.path.isfile(pj):
|
||||
return pj
|
||||
parent = os.path.dirname(d)
|
||||
if parent == d:
|
||||
break
|
||||
d = parent
|
||||
return None
|
||||
|
||||
|
||||
def _sg_get_edit_mode(cfg_dir):
|
||||
try:
|
||||
pj = _sg_find_v8project(os.getcwd()) or _sg_find_v8project(cfg_dir)
|
||||
if not pj:
|
||||
return "deny"
|
||||
proj = json.loads(open(pj, encoding="utf-8-sig").read())
|
||||
cfg_full = os.path.normcase(os.path.abspath(cfg_dir)).rstrip("\\/")
|
||||
for db in proj.get("databases", []):
|
||||
src = db.get("configSrc")
|
||||
if src:
|
||||
src_full = os.path.normcase(os.path.abspath(src)).rstrip("\\/")
|
||||
if cfg_full == src_full or cfg_full.startswith(src_full + os.sep):
|
||||
if db.get("editingAllowedCheck"):
|
||||
return db["editingAllowedCheck"]
|
||||
if proj.get("editingAllowedCheck"):
|
||||
return proj["editingAllowedCheck"]
|
||||
return "deny"
|
||||
except Exception:
|
||||
return "deny"
|
||||
|
||||
|
||||
def assert_edit_allowed(target_path, require):
|
||||
try:
|
||||
rp = os.path.abspath(target_path)
|
||||
elem_uuid = _sg_root_uuid(rp)
|
||||
cfg_dir = None
|
||||
bin_path = None
|
||||
d = rp if os.path.isdir(rp) else os.path.dirname(rp)
|
||||
for _ in range(12):
|
||||
if not d:
|
||||
break
|
||||
if not elem_uuid:
|
||||
elem_uuid = _sg_root_uuid(d + ".xml")
|
||||
if not cfg_dir:
|
||||
cand = os.path.join(d, "Ext", "ParentConfigurations.bin")
|
||||
if os.path.exists(cand) or os.path.exists(os.path.join(d, "Configuration.xml")):
|
||||
cfg_dir = d
|
||||
bin_path = cand
|
||||
if elem_uuid and cfg_dir:
|
||||
break
|
||||
parent = os.path.dirname(d)
|
||||
if parent == d:
|
||||
break
|
||||
d = parent
|
||||
if not elem_uuid and cfg_dir:
|
||||
elem_uuid = _sg_root_uuid(os.path.join(cfg_dir, "Configuration.xml"))
|
||||
if not bin_path or not os.path.exists(bin_path):
|
||||
return
|
||||
data = open(bin_path, "rb").read()
|
||||
if len(data) <= 32:
|
||||
return
|
||||
if data[:3] == b"\xef\xbb\xbf":
|
||||
data = data[3:]
|
||||
text = data.decode("utf-8", "replace")
|
||||
h = re.match(r"\{6,(\d+),(\d+),", text)
|
||||
if not h:
|
||||
return
|
||||
g = int(h.group(1))
|
||||
k = int(h.group(2))
|
||||
if k == 0:
|
||||
return
|
||||
best = None
|
||||
if elem_uuid:
|
||||
for m in re.finditer(r"([0-2]),0," + re.escape(elem_uuid.lower()), text):
|
||||
f1 = int(m.group(1))
|
||||
if best is None or f1 < best:
|
||||
best = f1
|
||||
blocked = False
|
||||
code = ""
|
||||
reason = ""
|
||||
if g == 1:
|
||||
blocked = True
|
||||
code = "capability-off"
|
||||
reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)"
|
||||
elif require == "removed":
|
||||
if best is not None and best != 2:
|
||||
blocked = True
|
||||
code = "not-removed"
|
||||
reason = "объект не снят с поддержки — удаление сломает обновления"
|
||||
else:
|
||||
if best is not None and best == 0:
|
||||
blocked = True
|
||||
code = "locked"
|
||||
reason = "объект на замке — редактирование сломает обновления"
|
||||
if not blocked:
|
||||
return
|
||||
mode = _sg_get_edit_mode(cfg_dir)
|
||||
if mode == "off":
|
||||
return
|
||||
if mode == "warn":
|
||||
sys.stderr.write(f"[support-guard] ПРЕДУПРЕЖДЕНИЕ: {reason}. Цель: {rp}\n")
|
||||
return
|
||||
head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления."
|
||||
cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются."
|
||||
off_note = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json."
|
||||
if code == "capability-off":
|
||||
state = f"Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «{rp}» редактировать нельзя."
|
||||
fix = (
|
||||
"Либо снять защиту явно (навык support-edit, два шага):\n"
|
||||
f' 1. support-edit -Path "{cfg_dir}" -Capability on — включить возможность изменения (объекты пока остаются на замке);\n'
|
||||
f' 2. support-edit -Path "{rp}" -Set editable — открыть этот объект для редактирования.\n'
|
||||
" Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора."
|
||||
)
|
||||
elif code == "not-removed":
|
||||
state = f"Состояние: объект «{rp}» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора."
|
||||
fix = (
|
||||
"Либо сначала снять объект с поддержки, затем удалять:\n"
|
||||
f' support-edit -Path "{rp}" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно.'
|
||||
)
|
||||
else:
|
||||
state = f"Состояние: объект «{rp}» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)."
|
||||
fix = (
|
||||
"Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):\n"
|
||||
f' support-edit -Path "{rp}" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);\n'
|
||||
f' support-edit -Path "{rp}" -Set off-support — снять с поддержки: обновления по объекту больше не приходят.'
|
||||
)
|
||||
sys.stderr.write(head + "\n" + state + "\n" + cfe + "\n" + fix + "\n" + off_note + "\n")
|
||||
sys.exit(1)
|
||||
except SystemExit:
|
||||
raise
|
||||
except Exception:
|
||||
return
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Inline utilities
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -83,6 +242,8 @@ with open(json_path, 'r', encoding='utf-8-sig') as f:
|
||||
|
||||
defn = json.loads(json_text)
|
||||
|
||||
assert_edit_allowed(output_dir, "editable")
|
||||
|
||||
# --- Batch mode: JSON array of objects ---
|
||||
if isinstance(defn, list):
|
||||
batch_ok = 0
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# meta-edit v1.6 — Edit existing 1C metadata object XML (inline mode + complex properties + TS attribute ops + modify-ts)
|
||||
# meta-edit v1.9 — Edit existing 1C metadata object XML (inline mode + complex properties + TS attribute ops + modify-ts)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[string]$DefinitionFile,
|
||||
@@ -152,6 +152,126 @@ if (-not (Test-Path $ObjectPath)) {
|
||||
}
|
||||
$resolvedPath = (Resolve-Path $ObjectPath).Path
|
||||
|
||||
# --- Support guard (Ext/ParentConfigurations.bin) ---
|
||||
# See docs/1c-support-state-spec.md. Blocks edits of vendor objects "на замке" /
|
||||
# read-only configs unless allowed. Trigger = bin present; reaction from
|
||||
# .v8-project.json editingAllowedCheck (deny|warn|off, default deny). Never
|
||||
# throws — guard errors degrade to allow.
|
||||
function Get-RootUuid([string]$xmlPath) {
|
||||
if (-not (Test-Path $xmlPath)) { return $null }
|
||||
try {
|
||||
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
|
||||
$el = $mx.DocumentElement.FirstChild
|
||||
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
|
||||
if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } }
|
||||
} catch {}
|
||||
return $null
|
||||
}
|
||||
function Find-V8Project([string]$startDir) {
|
||||
$d = $startDir
|
||||
for ($i = 0; $i -lt 20 -and $d; $i++) {
|
||||
$pj = Join-Path $d ".v8-project.json"
|
||||
if (Test-Path $pj) { return $pj }
|
||||
$parent = [System.IO.Path]::GetDirectoryName($d)
|
||||
if ($parent -eq $d) { break }
|
||||
$d = $parent
|
||||
}
|
||||
return $null
|
||||
}
|
||||
function Get-EditMode([string]$cfgDir) {
|
||||
try {
|
||||
$pj = Find-V8Project (Get-Location).Path
|
||||
if (-not $pj) { $pj = Find-V8Project $cfgDir }
|
||||
if (-not $pj) { return 'deny' }
|
||||
$proj = Get-Content -Raw $pj | ConvertFrom-Json
|
||||
$cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/')
|
||||
if ($proj.databases) {
|
||||
foreach ($db in $proj.databases) {
|
||||
if ($db.configSrc) {
|
||||
$src = [System.IO.Path]::GetFullPath($db.configSrc).TrimEnd('\', '/')
|
||||
if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) {
|
||||
if ($db.editingAllowedCheck) { return $db.editingAllowedCheck }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($proj.editingAllowedCheck) { return $proj.editingAllowedCheck }
|
||||
return 'deny'
|
||||
} catch { return 'deny' }
|
||||
}
|
||||
function Assert-EditAllowed([string]$targetPath, [string]$require) {
|
||||
try {
|
||||
$rp = $targetPath
|
||||
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
|
||||
$elemUuid = Get-RootUuid $rp
|
||||
$cfgDir = $null; $binPath = $null
|
||||
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
|
||||
for ($i = 0; $i -lt 12 -and $d; $i++) {
|
||||
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
|
||||
if (-not $cfgDir) {
|
||||
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
|
||||
if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $cfgDir = $d; $binPath = $cand }
|
||||
}
|
||||
if ($elemUuid -and $cfgDir) { break }
|
||||
$parent = [System.IO.Path]::GetDirectoryName($d)
|
||||
if ($parent -eq $d) { break }
|
||||
$d = $parent
|
||||
}
|
||||
# New object (no element file): fall back to config root uuid.
|
||||
if (-not $elemUuid -and $cfgDir) { $elemUuid = Get-RootUuid (Join-Path $cfgDir "Configuration.xml") }
|
||||
if (-not $binPath -or -not (Test-Path $binPath)) { return }
|
||||
$bytes = [System.IO.File]::ReadAllBytes($binPath)
|
||||
if ($bytes.Length -le 32) { return }
|
||||
$start = 0
|
||||
if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 }
|
||||
$text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start)
|
||||
$hm = [regex]::Match($text, '^\{6,(\d+),(\d+),')
|
||||
if (-not $hm.Success) { return }
|
||||
$G = [int]$hm.Groups[1].Value
|
||||
$K = [int]$hm.Groups[2].Value
|
||||
if ($K -eq 0) { return }
|
||||
$best = $null
|
||||
if ($elemUuid) {
|
||||
$u = [regex]::Escape($elemUuid.ToLower())
|
||||
foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) {
|
||||
$f1 = [int]$m.Groups[1].Value
|
||||
if ($null -eq $best -or $f1 -lt $best) { $best = $f1 }
|
||||
}
|
||||
}
|
||||
$blocked = $false; $code = ""; $reason = ""
|
||||
if ($G -eq 1) { $blocked = $true; $code = "capability-off"; $reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)" }
|
||||
elseif ($require -eq 'removed') {
|
||||
if ($null -ne $best -and $best -ne 2) { $blocked = $true; $code = "not-removed"; $reason = "объект не снят с поддержки — удаление сломает обновления" }
|
||||
}
|
||||
else {
|
||||
if ($null -ne $best -and $best -eq 0) { $blocked = $true; $code = "locked"; $reason = "объект на замке — редактирование сломает обновления" }
|
||||
}
|
||||
if (-not $blocked) { return }
|
||||
$mode = Get-EditMode $cfgDir
|
||||
if ($mode -eq 'off') { return }
|
||||
# Use Console.Error (not Write-Error) — under ErrorActionPreference=Stop the
|
||||
# latter throws and would be swallowed by this function's own catch.
|
||||
if ($mode -eq 'warn') { [Console]::Error.WriteLine("[support-guard] ПРЕДУПРЕЖДЕНИЕ: $reason. Цель: $rp"); return }
|
||||
$head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления."
|
||||
$cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются."
|
||||
$offNote = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json."
|
||||
if ($code -eq "capability-off") {
|
||||
$state = "Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «$rp» редактировать нельзя."
|
||||
$fix = "Либо снять защиту явно (навык support-edit, два шага):`n 1. support-edit -Path ""$cfgDir"" -Capability on — включить возможность изменения (объекты пока остаются на замке);`n 2. support-edit -Path ""$rp"" -Set editable — открыть этот объект для редактирования.`n Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора."
|
||||
} elseif ($code -eq "not-removed") {
|
||||
$state = "Состояние: объект «$rp» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора."
|
||||
$fix = "Либо сначала снять объект с поддержки, затем удалять:`n support-edit -Path ""$rp"" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно."
|
||||
} else {
|
||||
$state = "Состояние: объект «$rp» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)."
|
||||
$fix = "Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):`n support-edit -Path ""$rp"" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);`n support-edit -Path ""$rp"" -Set off-support — снять с поддержки: обновления по объекту больше не приходят."
|
||||
}
|
||||
[Console]::Error.WriteLine("$head`n$state`n$cfe`n$fix`n$offNote")
|
||||
exit 1
|
||||
} catch { return }
|
||||
}
|
||||
|
||||
Assert-EditAllowed $resolvedPath 'editable'
|
||||
|
||||
# --- Load XML ---
|
||||
$script:xmlDoc = New-Object System.Xml.XmlDocument
|
||||
$script:xmlDoc.PreserveWhitespace = $true
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# meta-edit v1.6 — Edit existing 1C metadata object XML (inline mode + complex properties + TS attribute ops + modify-ts)
|
||||
# meta-edit v1.9 — Edit existing 1C metadata object XML (inline mode + complex properties + TS attribute ops + modify-ts)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
@@ -11,6 +11,166 @@ import sys
|
||||
import uuid
|
||||
from lxml import etree
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md
|
||||
# Blocks edits of vendor objects "на замке" / read-only configs. Trigger = bin
|
||||
# present; reaction from .v8-project.json editingAllowedCheck (deny|warn|off,
|
||||
# default deny). Never throws (except sys.exit on deny) — errors degrade to allow.
|
||||
# ============================================================
|
||||
|
||||
def _sg_root_uuid(xml_path):
|
||||
if not os.path.isfile(xml_path):
|
||||
return None
|
||||
try:
|
||||
mx = etree.parse(xml_path).getroot()
|
||||
for child in mx:
|
||||
if isinstance(child.tag, str) and child.get("uuid"):
|
||||
return child.get("uuid")
|
||||
except Exception:
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def _sg_find_v8project(start_dir):
|
||||
d = start_dir
|
||||
for _ in range(20):
|
||||
if not d:
|
||||
break
|
||||
pj = os.path.join(d, ".v8-project.json")
|
||||
if os.path.isfile(pj):
|
||||
return pj
|
||||
parent = os.path.dirname(d)
|
||||
if parent == d:
|
||||
break
|
||||
d = parent
|
||||
return None
|
||||
|
||||
|
||||
def _sg_get_edit_mode(cfg_dir):
|
||||
try:
|
||||
pj = _sg_find_v8project(os.getcwd()) or _sg_find_v8project(cfg_dir)
|
||||
if not pj:
|
||||
return "deny"
|
||||
proj = json.loads(open(pj, encoding="utf-8-sig").read())
|
||||
cfg_full = os.path.normcase(os.path.abspath(cfg_dir)).rstrip("\\/")
|
||||
for db in proj.get("databases", []):
|
||||
src = db.get("configSrc")
|
||||
if src:
|
||||
src_full = os.path.normcase(os.path.abspath(src)).rstrip("\\/")
|
||||
if cfg_full == src_full or cfg_full.startswith(src_full + os.sep):
|
||||
if db.get("editingAllowedCheck"):
|
||||
return db["editingAllowedCheck"]
|
||||
if proj.get("editingAllowedCheck"):
|
||||
return proj["editingAllowedCheck"]
|
||||
return "deny"
|
||||
except Exception:
|
||||
return "deny"
|
||||
|
||||
|
||||
def assert_edit_allowed(target_path, require):
|
||||
try:
|
||||
rp = os.path.abspath(target_path)
|
||||
elem_uuid = _sg_root_uuid(rp)
|
||||
cfg_dir = None
|
||||
bin_path = None
|
||||
d = rp if os.path.isdir(rp) else os.path.dirname(rp)
|
||||
for _ in range(12):
|
||||
if not d:
|
||||
break
|
||||
if not elem_uuid:
|
||||
elem_uuid = _sg_root_uuid(d + ".xml")
|
||||
if not cfg_dir:
|
||||
cand = os.path.join(d, "Ext", "ParentConfigurations.bin")
|
||||
if os.path.exists(cand) or os.path.exists(os.path.join(d, "Configuration.xml")):
|
||||
cfg_dir = d
|
||||
bin_path = cand
|
||||
if elem_uuid and cfg_dir:
|
||||
break
|
||||
parent = os.path.dirname(d)
|
||||
if parent == d:
|
||||
break
|
||||
d = parent
|
||||
if not elem_uuid and cfg_dir:
|
||||
elem_uuid = _sg_root_uuid(os.path.join(cfg_dir, "Configuration.xml"))
|
||||
if not bin_path or not os.path.exists(bin_path):
|
||||
return
|
||||
data = open(bin_path, "rb").read()
|
||||
if len(data) <= 32:
|
||||
return
|
||||
if data[:3] == b"\xef\xbb\xbf":
|
||||
data = data[3:]
|
||||
text = data.decode("utf-8", "replace")
|
||||
h = re.match(r"\{6,(\d+),(\d+),", text)
|
||||
if not h:
|
||||
return
|
||||
g = int(h.group(1))
|
||||
k = int(h.group(2))
|
||||
if k == 0:
|
||||
return
|
||||
best = None
|
||||
if elem_uuid:
|
||||
for m in re.finditer(r"([0-2]),0," + re.escape(elem_uuid.lower()), text):
|
||||
f1 = int(m.group(1))
|
||||
if best is None or f1 < best:
|
||||
best = f1
|
||||
blocked = False
|
||||
code = ""
|
||||
reason = ""
|
||||
if g == 1:
|
||||
blocked = True
|
||||
code = "capability-off"
|
||||
reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)"
|
||||
elif require == "removed":
|
||||
if best is not None and best != 2:
|
||||
blocked = True
|
||||
code = "not-removed"
|
||||
reason = "объект не снят с поддержки — удаление сломает обновления"
|
||||
else:
|
||||
if best is not None and best == 0:
|
||||
blocked = True
|
||||
code = "locked"
|
||||
reason = "объект на замке — редактирование сломает обновления"
|
||||
if not blocked:
|
||||
return
|
||||
mode = _sg_get_edit_mode(cfg_dir)
|
||||
if mode == "off":
|
||||
return
|
||||
if mode == "warn":
|
||||
sys.stderr.write(f"[support-guard] ПРЕДУПРЕЖДЕНИЕ: {reason}. Цель: {rp}\n")
|
||||
return
|
||||
head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления."
|
||||
cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются."
|
||||
off_note = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json."
|
||||
if code == "capability-off":
|
||||
state = f"Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «{rp}» редактировать нельзя."
|
||||
fix = (
|
||||
"Либо снять защиту явно (навык support-edit, два шага):\n"
|
||||
f' 1. support-edit -Path "{cfg_dir}" -Capability on — включить возможность изменения (объекты пока остаются на замке);\n'
|
||||
f' 2. support-edit -Path "{rp}" -Set editable — открыть этот объект для редактирования.\n'
|
||||
" Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора."
|
||||
)
|
||||
elif code == "not-removed":
|
||||
state = f"Состояние: объект «{rp}» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора."
|
||||
fix = (
|
||||
"Либо сначала снять объект с поддержки, затем удалять:\n"
|
||||
f' support-edit -Path "{rp}" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно.'
|
||||
)
|
||||
else:
|
||||
state = f"Состояние: объект «{rp}» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)."
|
||||
fix = (
|
||||
"Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):\n"
|
||||
f' support-edit -Path "{rp}" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);\n'
|
||||
f' support-edit -Path "{rp}" -Set off-support — снять с поддержки: обновления по объекту больше не приходят.'
|
||||
)
|
||||
sys.stderr.write(head + "\n" + state + "\n" + cfe + "\n" + fix + "\n" + off_note + "\n")
|
||||
sys.exit(1)
|
||||
except SystemExit:
|
||||
raise
|
||||
except Exception:
|
||||
return
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Namespaces
|
||||
# ============================================================
|
||||
@@ -2169,6 +2329,8 @@ def main():
|
||||
|
||||
resolved_path = os.path.abspath(object_path)
|
||||
|
||||
assert_edit_allowed(resolved_path, "editable")
|
||||
|
||||
# --- Load XML ---
|
||||
xml_parser = etree.XMLParser(remove_blank_text=False)
|
||||
xml_tree = etree.parse(resolved_path, xml_parser)
|
||||
@@ -2257,7 +2419,7 @@ def main():
|
||||
print()
|
||||
print("--- Running meta-validate ---")
|
||||
python_exe = sys.executable
|
||||
subprocess.run([python_exe, validate_script, "-ObjectPath", "-Path", resolved_path])
|
||||
subprocess.run([python_exe, validate_script, "-ObjectPath", resolved_path])
|
||||
else:
|
||||
print()
|
||||
print(f"[SKIP] meta-validate not found at: {validate_script}")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# meta-info v1.2 — Compact summary of 1C metadata object
|
||||
# meta-info v1.3 — Compact summary of 1C metadata object
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][Alias('Path')][string]$ObjectPath,
|
||||
@@ -415,6 +415,51 @@ function Get-WSOperations($childObjs) {
|
||||
return $result
|
||||
}
|
||||
|
||||
# --- Support status of this object (Ext/ParentConfigurations.bin) ---
|
||||
# See docs/1c-support-state-spec.md. Walks up to the config root, decodes the
|
||||
# object's support rule. Never throws — degrades to "не на поддержке".
|
||||
function Get-ObjectSupportStatus([string]$objUuid) {
|
||||
try {
|
||||
# Walk up to the config root (dir with Configuration.xml or Ext/ParentConfigurations.bin).
|
||||
$d = [System.IO.Path]::GetDirectoryName($ObjectPath)
|
||||
$binPath = $null
|
||||
for ($i = 0; $i -lt 8 -and $d; $i++) {
|
||||
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
|
||||
if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $binPath = $cand; break }
|
||||
$parent = [System.IO.Path]::GetDirectoryName($d)
|
||||
if ($parent -eq $d) { break }
|
||||
$d = $parent
|
||||
}
|
||||
if (-not $binPath -or -not (Test-Path $binPath)) { return "не на поддержке" }
|
||||
$bytes = [System.IO.File]::ReadAllBytes($binPath)
|
||||
if ($bytes.Length -le 32) { return "снято с поддержки (правки свободны)" }
|
||||
$start = 0
|
||||
if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 }
|
||||
$text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start)
|
||||
$h = [regex]::Match($text, '^\{6,(\d+),(\d+),')
|
||||
if (-not $h.Success) { return "не на поддержке" }
|
||||
$G = [int]$h.Groups[1].Value
|
||||
$K = [int]$h.Groups[2].Value
|
||||
if ($K -eq 0) { return "снято с поддержки (правки свободны)" }
|
||||
if ($G -eq 1) { return "конфигурация read-only (возможность изменения выключена) — правки невозможны без включения" }
|
||||
if (-not $objUuid) { return "не на поддержке" }
|
||||
# Conservative fold: locked if f1=0 in ANY vendor block.
|
||||
$u = [regex]::Escape($objUuid.ToLower())
|
||||
$best = $null
|
||||
foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) {
|
||||
$f1 = [int]$m.Groups[1].Value
|
||||
if ($null -eq $best -or $f1 -lt $best) { $best = $f1 }
|
||||
}
|
||||
if ($null -eq $best) { return "не на поддержке" }
|
||||
switch ($best) {
|
||||
0 { return "на замке — прямая правка сломает обновления; дорабатывай через cfe-* либо включи редактирование объекта" }
|
||||
1 { return "редактируется с сохранением поддержки" }
|
||||
2 { return "снято с поддержки (правки свободны)" }
|
||||
}
|
||||
return "не на поддержке"
|
||||
} catch { return "не на поддержке" }
|
||||
}
|
||||
|
||||
# --- Extract metadata ---
|
||||
$props = $typeNode.SelectSingleNode("md:Properties", $ns)
|
||||
$childObjs = $typeNode.SelectSingleNode("md:ChildObjects", $ns)
|
||||
@@ -608,6 +653,7 @@ if (-not $drillDone) {
|
||||
if ($synonym -and $synonym -ne $objName) { $header += " — `"$synonym`"" }
|
||||
$header += " ==="
|
||||
Out $header
|
||||
Out "Поддержка: $(Get-ObjectSupportStatus $typeNode.GetAttribute('uuid'))"
|
||||
|
||||
# --- Type presentation (ref objects) ---
|
||||
if ($isRefObject) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# meta-info v1.2 — Compact summary of 1C metadata object (Python port)
|
||||
# meta-info v1.3 — Compact summary of 1C metadata object (Python port)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
import argparse
|
||||
import os
|
||||
@@ -469,6 +469,59 @@ def get_ws_operations(child_objs):
|
||||
return result
|
||||
|
||||
|
||||
# ── Support status of this object (Ext/ParentConfigurations.bin) ──
|
||||
# See docs/1c-support-state-spec.md. Walks up to the config root, decodes the
|
||||
# object's support rule. Never throws — degrades to "не на поддержке".
|
||||
def get_object_support_status(obj_uuid):
|
||||
try:
|
||||
d = os.path.dirname(object_path)
|
||||
bin_path = None
|
||||
for _ in range(8):
|
||||
if not d:
|
||||
break
|
||||
cand = os.path.join(d, "Ext", "ParentConfigurations.bin")
|
||||
if os.path.exists(cand) or os.path.exists(os.path.join(d, "Configuration.xml")):
|
||||
bin_path = cand
|
||||
break
|
||||
parent = os.path.dirname(d)
|
||||
if parent == d:
|
||||
break
|
||||
d = parent
|
||||
if not bin_path or not os.path.exists(bin_path):
|
||||
return "не на поддержке"
|
||||
data = open(bin_path, "rb").read()
|
||||
if len(data) <= 32:
|
||||
return "снято с поддержки (правки свободны)"
|
||||
if data[:3] == b"\xef\xbb\xbf":
|
||||
data = data[3:]
|
||||
text = data.decode("utf-8", "replace")
|
||||
h = re.match(r"\{6,(\d+),(\d+),", text)
|
||||
if not h:
|
||||
return "не на поддержке"
|
||||
g = int(h.group(1))
|
||||
k = int(h.group(2))
|
||||
if k == 0:
|
||||
return "снято с поддержки (правки свободны)"
|
||||
if g == 1:
|
||||
return "конфигурация read-only (возможность изменения выключена) — правки невозможны без включения"
|
||||
if not obj_uuid:
|
||||
return "не на поддержке"
|
||||
best = None
|
||||
for m in re.finditer(r"([0-2]),0," + re.escape(obj_uuid.lower()), text):
|
||||
f1 = int(m.group(1))
|
||||
if best is None or f1 < best:
|
||||
best = f1
|
||||
if best is None:
|
||||
return "не на поддержке"
|
||||
return {
|
||||
0: "на замке — прямая правка сломает обновления; дорабатывай через cfe-* либо включи редактирование объекта",
|
||||
1: "редактируется с сохранением поддержки",
|
||||
2: "снято с поддержки (правки свободны)",
|
||||
}.get(best, "не на поддержке")
|
||||
except Exception:
|
||||
return "не на поддержке"
|
||||
|
||||
|
||||
# ── Extract metadata ─────────────────────────────────────────
|
||||
|
||||
props = find(type_node, "md:Properties")
|
||||
@@ -650,6 +703,7 @@ if not drill_done:
|
||||
header += f' \u2014 "{synonym}"'
|
||||
header += " ==="
|
||||
out(header)
|
||||
out(f"Поддержка: {get_object_support_status(type_node.get('uuid', ''))}")
|
||||
|
||||
# Type presentation (ref objects)
|
||||
if is_ref_object:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# meta-remove v1.1 — Remove metadata object from 1C configuration dump
|
||||
# meta-remove v1.3 — Remove metadata object from 1C configuration dump
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
@@ -78,6 +78,124 @@ if (-not (Test-Path $configXml)) {
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Support guard (Ext/ParentConfigurations.bin) ---
|
||||
# See docs/1c-support-state-spec.md. Blocks edits of vendor objects "на замке" /
|
||||
# read-only configs unless allowed. Trigger = bin present; reaction from
|
||||
# .v8-project.json editingAllowedCheck (deny|warn|off, default deny). Never
|
||||
# throws — guard errors degrade to allow.
|
||||
function Get-RootUuid([string]$xmlPath) {
|
||||
if (-not (Test-Path $xmlPath)) { return $null }
|
||||
try {
|
||||
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
|
||||
$el = $mx.DocumentElement.FirstChild
|
||||
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
|
||||
if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } }
|
||||
} catch {}
|
||||
return $null
|
||||
}
|
||||
function Find-V8Project([string]$startDir) {
|
||||
$d = $startDir
|
||||
for ($i = 0; $i -lt 20 -and $d; $i++) {
|
||||
$pj = Join-Path $d ".v8-project.json"
|
||||
if (Test-Path $pj) { return $pj }
|
||||
$parent = [System.IO.Path]::GetDirectoryName($d)
|
||||
if ($parent -eq $d) { break }
|
||||
$d = $parent
|
||||
}
|
||||
return $null
|
||||
}
|
||||
function Get-EditMode([string]$cfgDir) {
|
||||
try {
|
||||
$pj = Find-V8Project (Get-Location).Path
|
||||
if (-not $pj) { $pj = Find-V8Project $cfgDir }
|
||||
if (-not $pj) { return 'deny' }
|
||||
$proj = Get-Content -Raw $pj | ConvertFrom-Json
|
||||
$cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/')
|
||||
if ($proj.databases) {
|
||||
foreach ($db in $proj.databases) {
|
||||
if ($db.configSrc) {
|
||||
$src = [System.IO.Path]::GetFullPath($db.configSrc).TrimEnd('\', '/')
|
||||
if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) {
|
||||
if ($db.editingAllowedCheck) { return $db.editingAllowedCheck }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($proj.editingAllowedCheck) { return $proj.editingAllowedCheck }
|
||||
return 'deny'
|
||||
} catch { return 'deny' }
|
||||
}
|
||||
function Assert-EditAllowed([string]$targetPath, [string]$require) {
|
||||
try {
|
||||
$rp = $targetPath
|
||||
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
|
||||
$elemUuid = Get-RootUuid $rp
|
||||
$cfgDir = $null; $binPath = $null
|
||||
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
|
||||
for ($i = 0; $i -lt 12 -and $d; $i++) {
|
||||
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
|
||||
if (-not $cfgDir) {
|
||||
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
|
||||
if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $cfgDir = $d; $binPath = $cand }
|
||||
}
|
||||
if ($elemUuid -and $cfgDir) { break }
|
||||
$parent = [System.IO.Path]::GetDirectoryName($d)
|
||||
if ($parent -eq $d) { break }
|
||||
$d = $parent
|
||||
}
|
||||
# New object (no element file): fall back to config root uuid.
|
||||
if (-not $elemUuid -and $cfgDir) { $elemUuid = Get-RootUuid (Join-Path $cfgDir "Configuration.xml") }
|
||||
if (-not $binPath -or -not (Test-Path $binPath)) { return }
|
||||
$bytes = [System.IO.File]::ReadAllBytes($binPath)
|
||||
if ($bytes.Length -le 32) { return }
|
||||
$start = 0
|
||||
if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 }
|
||||
$text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start)
|
||||
$hm = [regex]::Match($text, '^\{6,(\d+),(\d+),')
|
||||
if (-not $hm.Success) { return }
|
||||
$G = [int]$hm.Groups[1].Value
|
||||
$K = [int]$hm.Groups[2].Value
|
||||
if ($K -eq 0) { return }
|
||||
$best = $null
|
||||
if ($elemUuid) {
|
||||
$u = [regex]::Escape($elemUuid.ToLower())
|
||||
foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) {
|
||||
$f1 = [int]$m.Groups[1].Value
|
||||
if ($null -eq $best -or $f1 -lt $best) { $best = $f1 }
|
||||
}
|
||||
}
|
||||
$blocked = $false; $code = ""; $reason = ""
|
||||
if ($G -eq 1) { $blocked = $true; $code = "capability-off"; $reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)" }
|
||||
elseif ($require -eq 'removed') {
|
||||
if ($null -ne $best -and $best -ne 2) { $blocked = $true; $code = "not-removed"; $reason = "объект не снят с поддержки — удаление сломает обновления" }
|
||||
}
|
||||
else {
|
||||
if ($null -ne $best -and $best -eq 0) { $blocked = $true; $code = "locked"; $reason = "объект на замке — редактирование сломает обновления" }
|
||||
}
|
||||
if (-not $blocked) { return }
|
||||
$mode = Get-EditMode $cfgDir
|
||||
if ($mode -eq 'off') { return }
|
||||
# Use Console.Error (not Write-Error) — under ErrorActionPreference=Stop the
|
||||
# latter throws and would be swallowed by this function's own catch.
|
||||
if ($mode -eq 'warn') { [Console]::Error.WriteLine("[support-guard] ПРЕДУПРЕЖДЕНИЕ: $reason. Цель: $rp"); return }
|
||||
$head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления."
|
||||
$cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются."
|
||||
$offNote = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json."
|
||||
if ($code -eq "capability-off") {
|
||||
$state = "Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «$rp» редактировать нельзя."
|
||||
$fix = "Либо снять защиту явно (навык support-edit, два шага):`n 1. support-edit -Path ""$cfgDir"" -Capability on — включить возможность изменения (объекты пока остаются на замке);`n 2. support-edit -Path ""$rp"" -Set editable — открыть этот объект для редактирования.`n Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора."
|
||||
} elseif ($code -eq "not-removed") {
|
||||
$state = "Состояние: объект «$rp» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора."
|
||||
$fix = "Либо сначала снять объект с поддержки, затем удалять:`n support-edit -Path ""$rp"" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно."
|
||||
} else {
|
||||
$state = "Состояние: объект «$rp» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)."
|
||||
$fix = "Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):`n support-edit -Path ""$rp"" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);`n support-edit -Path ""$rp"" -Set off-support — снять с поддержки: обновления по объекту больше не приходят."
|
||||
}
|
||||
[Console]::Error.WriteLine("$head`n$state`n$cfe`n$fix`n$offNote")
|
||||
exit 1
|
||||
} catch { return }
|
||||
}
|
||||
|
||||
# --- Parse object spec ---
|
||||
|
||||
$parts = $Object -split "\.", 2
|
||||
@@ -113,6 +231,9 @@ $typeDir = Join-Path $ConfigDir $typePlural
|
||||
$objXml = Join-Path $typeDir "$objName.xml"
|
||||
$objDir = Join-Path $typeDir $objName
|
||||
|
||||
# Support guard — removal requires the object be снят-с-поддержки (f1=2).
|
||||
Assert-EditAllowed $objXml 'removed'
|
||||
|
||||
$hasXml = Test-Path $objXml
|
||||
$hasDir = Test-Path $objDir -PathType Container
|
||||
|
||||
|
||||
@@ -1,13 +1,174 @@
|
||||
#!/usr/bin/env python3
|
||||
# meta-remove v1.1 — Remove metadata object from 1C configuration dump
|
||||
# meta-remove v1.3 — Remove metadata object from 1C configuration dump
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import shutil
|
||||
from lxml import etree
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md
|
||||
# Blocks edits of vendor objects "на замке" / read-only configs. Trigger = bin
|
||||
# present; reaction from .v8-project.json editingAllowedCheck (deny|warn|off,
|
||||
# default deny). Never throws (except sys.exit on deny) — errors degrade to allow.
|
||||
# ============================================================
|
||||
|
||||
def _sg_root_uuid(xml_path):
|
||||
if not os.path.isfile(xml_path):
|
||||
return None
|
||||
try:
|
||||
mx = etree.parse(xml_path).getroot()
|
||||
for child in mx:
|
||||
if isinstance(child.tag, str) and child.get("uuid"):
|
||||
return child.get("uuid")
|
||||
except Exception:
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def _sg_find_v8project(start_dir):
|
||||
d = start_dir
|
||||
for _ in range(20):
|
||||
if not d:
|
||||
break
|
||||
pj = os.path.join(d, ".v8-project.json")
|
||||
if os.path.isfile(pj):
|
||||
return pj
|
||||
parent = os.path.dirname(d)
|
||||
if parent == d:
|
||||
break
|
||||
d = parent
|
||||
return None
|
||||
|
||||
|
||||
def _sg_get_edit_mode(cfg_dir):
|
||||
try:
|
||||
pj = _sg_find_v8project(os.getcwd()) or _sg_find_v8project(cfg_dir)
|
||||
if not pj:
|
||||
return "deny"
|
||||
proj = json.loads(open(pj, encoding="utf-8-sig").read())
|
||||
cfg_full = os.path.normcase(os.path.abspath(cfg_dir)).rstrip("\\/")
|
||||
for db in proj.get("databases", []):
|
||||
src = db.get("configSrc")
|
||||
if src:
|
||||
src_full = os.path.normcase(os.path.abspath(src)).rstrip("\\/")
|
||||
if cfg_full == src_full or cfg_full.startswith(src_full + os.sep):
|
||||
if db.get("editingAllowedCheck"):
|
||||
return db["editingAllowedCheck"]
|
||||
if proj.get("editingAllowedCheck"):
|
||||
return proj["editingAllowedCheck"]
|
||||
return "deny"
|
||||
except Exception:
|
||||
return "deny"
|
||||
|
||||
|
||||
def assert_edit_allowed(target_path, require):
|
||||
try:
|
||||
rp = os.path.abspath(target_path)
|
||||
elem_uuid = _sg_root_uuid(rp)
|
||||
cfg_dir = None
|
||||
bin_path = None
|
||||
d = rp if os.path.isdir(rp) else os.path.dirname(rp)
|
||||
for _ in range(12):
|
||||
if not d:
|
||||
break
|
||||
if not elem_uuid:
|
||||
elem_uuid = _sg_root_uuid(d + ".xml")
|
||||
if not cfg_dir:
|
||||
cand = os.path.join(d, "Ext", "ParentConfigurations.bin")
|
||||
if os.path.exists(cand) or os.path.exists(os.path.join(d, "Configuration.xml")):
|
||||
cfg_dir = d
|
||||
bin_path = cand
|
||||
if elem_uuid and cfg_dir:
|
||||
break
|
||||
parent = os.path.dirname(d)
|
||||
if parent == d:
|
||||
break
|
||||
d = parent
|
||||
if not elem_uuid and cfg_dir:
|
||||
elem_uuid = _sg_root_uuid(os.path.join(cfg_dir, "Configuration.xml"))
|
||||
if not bin_path or not os.path.exists(bin_path):
|
||||
return
|
||||
data = open(bin_path, "rb").read()
|
||||
if len(data) <= 32:
|
||||
return
|
||||
if data[:3] == b"\xef\xbb\xbf":
|
||||
data = data[3:]
|
||||
text = data.decode("utf-8", "replace")
|
||||
h = re.match(r"\{6,(\d+),(\d+),", text)
|
||||
if not h:
|
||||
return
|
||||
g = int(h.group(1))
|
||||
k = int(h.group(2))
|
||||
if k == 0:
|
||||
return
|
||||
best = None
|
||||
if elem_uuid:
|
||||
for m in re.finditer(r"([0-2]),0," + re.escape(elem_uuid.lower()), text):
|
||||
f1 = int(m.group(1))
|
||||
if best is None or f1 < best:
|
||||
best = f1
|
||||
blocked = False
|
||||
code = ""
|
||||
reason = ""
|
||||
if g == 1:
|
||||
blocked = True
|
||||
code = "capability-off"
|
||||
reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)"
|
||||
elif require == "removed":
|
||||
if best is not None and best != 2:
|
||||
blocked = True
|
||||
code = "not-removed"
|
||||
reason = "объект не снят с поддержки — удаление сломает обновления"
|
||||
else:
|
||||
if best is not None and best == 0:
|
||||
blocked = True
|
||||
code = "locked"
|
||||
reason = "объект на замке — редактирование сломает обновления"
|
||||
if not blocked:
|
||||
return
|
||||
mode = _sg_get_edit_mode(cfg_dir)
|
||||
if mode == "off":
|
||||
return
|
||||
if mode == "warn":
|
||||
sys.stderr.write(f"[support-guard] ПРЕДУПРЕЖДЕНИЕ: {reason}. Цель: {rp}\n")
|
||||
return
|
||||
head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления."
|
||||
cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются."
|
||||
off_note = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json."
|
||||
if code == "capability-off":
|
||||
state = f"Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «{rp}» редактировать нельзя."
|
||||
fix = (
|
||||
"Либо снять защиту явно (навык support-edit, два шага):\n"
|
||||
f' 1. support-edit -Path "{cfg_dir}" -Capability on — включить возможность изменения (объекты пока остаются на замке);\n'
|
||||
f' 2. support-edit -Path "{rp}" -Set editable — открыть этот объект для редактирования.\n'
|
||||
" Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора."
|
||||
)
|
||||
elif code == "not-removed":
|
||||
state = f"Состояние: объект «{rp}» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора."
|
||||
fix = (
|
||||
"Либо сначала снять объект с поддержки, затем удалять:\n"
|
||||
f' support-edit -Path "{rp}" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно.'
|
||||
)
|
||||
else:
|
||||
state = f"Состояние: объект «{rp}» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)."
|
||||
fix = (
|
||||
"Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):\n"
|
||||
f' support-edit -Path "{rp}" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);\n'
|
||||
f' support-edit -Path "{rp}" -Set off-support — снять с поддержки: обновления по объекту больше не приходят.'
|
||||
)
|
||||
sys.stderr.write(head + "\n" + state + "\n" + cfe + "\n" + fix + "\n" + off_note + "\n")
|
||||
sys.exit(1)
|
||||
except SystemExit:
|
||||
raise
|
||||
except Exception:
|
||||
return
|
||||
|
||||
# --- Type -> plural directory mapping ---
|
||||
|
||||
TYPE_PLURAL_MAP = {
|
||||
@@ -161,6 +322,9 @@ def main():
|
||||
obj_xml = os.path.join(type_dir, f"{obj_name}.xml")
|
||||
obj_dir = os.path.join(type_dir, obj_name)
|
||||
|
||||
# Support guard — removal requires the object be снят-с-поддержки (f1=2).
|
||||
assert_edit_allowed(obj_xml, "removed")
|
||||
|
||||
has_xml = os.path.isfile(obj_xml)
|
||||
has_dir = os.path.isdir(obj_dir)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# meta-validate v1.3 — Validate 1C metadata object structure
|
||||
# meta-validate v1.4 — Validate 1C metadata object structure
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# meta-validate v1.3 — Validate 1C metadata object structure (Python port)
|
||||
# meta-validate v1.4 — Validate 1C metadata object structure (Python port)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
import argparse
|
||||
import os
|
||||
@@ -31,7 +31,7 @@ if len(path_list) > 1:
|
||||
batch_ok = 0
|
||||
batch_fail = 0
|
||||
for single_path in path_list:
|
||||
cmd = [sys.executable, __file__, "-ObjectPath", "-Path", single_path, "-MaxErrors", str(max_errors)]
|
||||
cmd = [sys.executable, __file__, "-ObjectPath", single_path, "-MaxErrors", str(max_errors)]
|
||||
if detailed:
|
||||
cmd.append("-Detailed")
|
||||
if out_file:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# mxl-compile v1.1 — Compile 1C spreadsheet from JSON
|
||||
# mxl-compile v1.3 — Compile 1C spreadsheet from JSON
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
@@ -11,6 +11,124 @@ param(
|
||||
$ErrorActionPreference = "Stop"
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# --- Support guard (Ext/ParentConfigurations.bin) ---
|
||||
# See docs/1c-support-state-spec.md. Blocks edits of vendor objects "на замке" /
|
||||
# read-only configs unless allowed. Trigger = bin present; reaction from
|
||||
# .v8-project.json editingAllowedCheck (deny|warn|off, default deny). Never
|
||||
# throws — guard errors degrade to allow.
|
||||
function Get-RootUuid([string]$xmlPath) {
|
||||
if (-not (Test-Path $xmlPath)) { return $null }
|
||||
try {
|
||||
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
|
||||
$el = $mx.DocumentElement.FirstChild
|
||||
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
|
||||
if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } }
|
||||
} catch {}
|
||||
return $null
|
||||
}
|
||||
function Find-V8Project([string]$startDir) {
|
||||
$d = $startDir
|
||||
for ($i = 0; $i -lt 20 -and $d; $i++) {
|
||||
$pj = Join-Path $d ".v8-project.json"
|
||||
if (Test-Path $pj) { return $pj }
|
||||
$parent = [System.IO.Path]::GetDirectoryName($d)
|
||||
if ($parent -eq $d) { break }
|
||||
$d = $parent
|
||||
}
|
||||
return $null
|
||||
}
|
||||
function Get-EditMode([string]$cfgDir) {
|
||||
try {
|
||||
$pj = Find-V8Project (Get-Location).Path
|
||||
if (-not $pj) { $pj = Find-V8Project $cfgDir }
|
||||
if (-not $pj) { return 'deny' }
|
||||
$proj = Get-Content -Raw $pj | ConvertFrom-Json
|
||||
$cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/')
|
||||
if ($proj.databases) {
|
||||
foreach ($db in $proj.databases) {
|
||||
if ($db.configSrc) {
|
||||
$src = [System.IO.Path]::GetFullPath($db.configSrc).TrimEnd('\', '/')
|
||||
if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) {
|
||||
if ($db.editingAllowedCheck) { return $db.editingAllowedCheck }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($proj.editingAllowedCheck) { return $proj.editingAllowedCheck }
|
||||
return 'deny'
|
||||
} catch { return 'deny' }
|
||||
}
|
||||
function Assert-EditAllowed([string]$targetPath, [string]$require) {
|
||||
try {
|
||||
$rp = $targetPath
|
||||
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
|
||||
$elemUuid = Get-RootUuid $rp
|
||||
$cfgDir = $null; $binPath = $null
|
||||
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
|
||||
for ($i = 0; $i -lt 12 -and $d; $i++) {
|
||||
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
|
||||
if (-not $cfgDir) {
|
||||
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
|
||||
if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $cfgDir = $d; $binPath = $cand }
|
||||
}
|
||||
if ($elemUuid -and $cfgDir) { break }
|
||||
$parent = [System.IO.Path]::GetDirectoryName($d)
|
||||
if ($parent -eq $d) { break }
|
||||
$d = $parent
|
||||
}
|
||||
# New object (no element file): fall back to config root uuid.
|
||||
if (-not $elemUuid -and $cfgDir) { $elemUuid = Get-RootUuid (Join-Path $cfgDir "Configuration.xml") }
|
||||
if (-not $binPath -or -not (Test-Path $binPath)) { return }
|
||||
$bytes = [System.IO.File]::ReadAllBytes($binPath)
|
||||
if ($bytes.Length -le 32) { return }
|
||||
$start = 0
|
||||
if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 }
|
||||
$text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start)
|
||||
$hm = [regex]::Match($text, '^\{6,(\d+),(\d+),')
|
||||
if (-not $hm.Success) { return }
|
||||
$G = [int]$hm.Groups[1].Value
|
||||
$K = [int]$hm.Groups[2].Value
|
||||
if ($K -eq 0) { return }
|
||||
$best = $null
|
||||
if ($elemUuid) {
|
||||
$u = [regex]::Escape($elemUuid.ToLower())
|
||||
foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) {
|
||||
$f1 = [int]$m.Groups[1].Value
|
||||
if ($null -eq $best -or $f1 -lt $best) { $best = $f1 }
|
||||
}
|
||||
}
|
||||
$blocked = $false; $code = ""; $reason = ""
|
||||
if ($G -eq 1) { $blocked = $true; $code = "capability-off"; $reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)" }
|
||||
elseif ($require -eq 'removed') {
|
||||
if ($null -ne $best -and $best -ne 2) { $blocked = $true; $code = "not-removed"; $reason = "объект не снят с поддержки — удаление сломает обновления" }
|
||||
}
|
||||
else {
|
||||
if ($null -ne $best -and $best -eq 0) { $blocked = $true; $code = "locked"; $reason = "объект на замке — редактирование сломает обновления" }
|
||||
}
|
||||
if (-not $blocked) { return }
|
||||
$mode = Get-EditMode $cfgDir
|
||||
if ($mode -eq 'off') { return }
|
||||
# Use Console.Error (not Write-Error) — under ErrorActionPreference=Stop the
|
||||
# latter throws and would be swallowed by this function's own catch.
|
||||
if ($mode -eq 'warn') { [Console]::Error.WriteLine("[support-guard] ПРЕДУПРЕЖДЕНИЕ: $reason. Цель: $rp"); return }
|
||||
$head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления."
|
||||
$cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются."
|
||||
$offNote = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json."
|
||||
if ($code -eq "capability-off") {
|
||||
$state = "Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «$rp» редактировать нельзя."
|
||||
$fix = "Либо снять защиту явно (навык support-edit, два шага):`n 1. support-edit -Path ""$cfgDir"" -Capability on — включить возможность изменения (объекты пока остаются на замке);`n 2. support-edit -Path ""$rp"" -Set editable — открыть этот объект для редактирования.`n Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора."
|
||||
} elseif ($code -eq "not-removed") {
|
||||
$state = "Состояние: объект «$rp» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора."
|
||||
$fix = "Либо сначала снять объект с поддержки, затем удалять:`n support-edit -Path ""$rp"" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно."
|
||||
} else {
|
||||
$state = "Состояние: объект «$rp» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)."
|
||||
$fix = "Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):`n support-edit -Path ""$rp"" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);`n support-edit -Path ""$rp"" -Set off-support — снять с поддержки: обновления по объекту больше не приходят."
|
||||
}
|
||||
[Console]::Error.WriteLine("$head`n$state`n$cfe`n$fix`n$offNote")
|
||||
exit 1
|
||||
} catch { return }
|
||||
}
|
||||
|
||||
# --- 1. Load and validate JSON ---
|
||||
|
||||
if (-not (Test-Path $JsonPath)) {
|
||||
@@ -720,6 +838,7 @@ X '</document>'
|
||||
|
||||
$enc = New-Object System.Text.UTF8Encoding($true)
|
||||
$resolvedPath = if ([System.IO.Path]::IsPathRooted($OutputPath)) { $OutputPath } else { Join-Path (Get-Location) $OutputPath }
|
||||
Assert-EditAllowed $resolvedPath 'editable'
|
||||
[System.IO.File]::WriteAllText($resolvedPath, $xml.ToString(), $enc)
|
||||
|
||||
# --- 9. Summary ---
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# mxl-compile v1.1 — Compile 1C spreadsheet from JSON
|
||||
# mxl-compile v1.3 — Compile 1C spreadsheet from JSON
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
import argparse
|
||||
import json
|
||||
@@ -8,6 +8,167 @@ import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
from lxml import etree
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md
|
||||
# Blocks edits of vendor objects "на замке" / read-only configs. Trigger = bin
|
||||
# present; reaction from .v8-project.json editingAllowedCheck (deny|warn|off,
|
||||
# default deny). Never throws (except sys.exit on deny) — errors degrade to allow.
|
||||
# ============================================================
|
||||
|
||||
def _sg_root_uuid(xml_path):
|
||||
if not os.path.isfile(xml_path):
|
||||
return None
|
||||
try:
|
||||
mx = etree.parse(xml_path).getroot()
|
||||
for child in mx:
|
||||
if isinstance(child.tag, str) and child.get("uuid"):
|
||||
return child.get("uuid")
|
||||
except Exception:
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def _sg_find_v8project(start_dir):
|
||||
d = start_dir
|
||||
for _ in range(20):
|
||||
if not d:
|
||||
break
|
||||
pj = os.path.join(d, ".v8-project.json")
|
||||
if os.path.isfile(pj):
|
||||
return pj
|
||||
parent = os.path.dirname(d)
|
||||
if parent == d:
|
||||
break
|
||||
d = parent
|
||||
return None
|
||||
|
||||
|
||||
def _sg_get_edit_mode(cfg_dir):
|
||||
try:
|
||||
pj = _sg_find_v8project(os.getcwd()) or _sg_find_v8project(cfg_dir)
|
||||
if not pj:
|
||||
return "deny"
|
||||
proj = json.loads(open(pj, encoding="utf-8-sig").read())
|
||||
cfg_full = os.path.normcase(os.path.abspath(cfg_dir)).rstrip("\\/")
|
||||
for db in proj.get("databases", []):
|
||||
src = db.get("configSrc")
|
||||
if src:
|
||||
src_full = os.path.normcase(os.path.abspath(src)).rstrip("\\/")
|
||||
if cfg_full == src_full or cfg_full.startswith(src_full + os.sep):
|
||||
if db.get("editingAllowedCheck"):
|
||||
return db["editingAllowedCheck"]
|
||||
if proj.get("editingAllowedCheck"):
|
||||
return proj["editingAllowedCheck"]
|
||||
return "deny"
|
||||
except Exception:
|
||||
return "deny"
|
||||
|
||||
|
||||
def assert_edit_allowed(target_path, require):
|
||||
try:
|
||||
rp = os.path.abspath(target_path)
|
||||
elem_uuid = _sg_root_uuid(rp)
|
||||
cfg_dir = None
|
||||
bin_path = None
|
||||
d = rp if os.path.isdir(rp) else os.path.dirname(rp)
|
||||
for _ in range(12):
|
||||
if not d:
|
||||
break
|
||||
if not elem_uuid:
|
||||
elem_uuid = _sg_root_uuid(d + ".xml")
|
||||
if not cfg_dir:
|
||||
cand = os.path.join(d, "Ext", "ParentConfigurations.bin")
|
||||
if os.path.exists(cand) or os.path.exists(os.path.join(d, "Configuration.xml")):
|
||||
cfg_dir = d
|
||||
bin_path = cand
|
||||
if elem_uuid and cfg_dir:
|
||||
break
|
||||
parent = os.path.dirname(d)
|
||||
if parent == d:
|
||||
break
|
||||
d = parent
|
||||
if not elem_uuid and cfg_dir:
|
||||
elem_uuid = _sg_root_uuid(os.path.join(cfg_dir, "Configuration.xml"))
|
||||
if not bin_path or not os.path.exists(bin_path):
|
||||
return
|
||||
data = open(bin_path, "rb").read()
|
||||
if len(data) <= 32:
|
||||
return
|
||||
if data[:3] == b"\xef\xbb\xbf":
|
||||
data = data[3:]
|
||||
text = data.decode("utf-8", "replace")
|
||||
h = re.match(r"\{6,(\d+),(\d+),", text)
|
||||
if not h:
|
||||
return
|
||||
g = int(h.group(1))
|
||||
k = int(h.group(2))
|
||||
if k == 0:
|
||||
return
|
||||
best = None
|
||||
if elem_uuid:
|
||||
for m in re.finditer(r"([0-2]),0," + re.escape(elem_uuid.lower()), text):
|
||||
f1 = int(m.group(1))
|
||||
if best is None or f1 < best:
|
||||
best = f1
|
||||
blocked = False
|
||||
code = ""
|
||||
reason = ""
|
||||
if g == 1:
|
||||
blocked = True
|
||||
code = "capability-off"
|
||||
reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)"
|
||||
elif require == "removed":
|
||||
if best is not None and best != 2:
|
||||
blocked = True
|
||||
code = "not-removed"
|
||||
reason = "объект не снят с поддержки — удаление сломает обновления"
|
||||
else:
|
||||
if best is not None and best == 0:
|
||||
blocked = True
|
||||
code = "locked"
|
||||
reason = "объект на замке — редактирование сломает обновления"
|
||||
if not blocked:
|
||||
return
|
||||
mode = _sg_get_edit_mode(cfg_dir)
|
||||
if mode == "off":
|
||||
return
|
||||
if mode == "warn":
|
||||
sys.stderr.write(f"[support-guard] ПРЕДУПРЕЖДЕНИЕ: {reason}. Цель: {rp}\n")
|
||||
return
|
||||
head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления."
|
||||
cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются."
|
||||
off_note = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json."
|
||||
if code == "capability-off":
|
||||
state = f"Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «{rp}» редактировать нельзя."
|
||||
fix = (
|
||||
"Либо снять защиту явно (навык support-edit, два шага):\n"
|
||||
f' 1. support-edit -Path "{cfg_dir}" -Capability on — включить возможность изменения (объекты пока остаются на замке);\n'
|
||||
f' 2. support-edit -Path "{rp}" -Set editable — открыть этот объект для редактирования.\n'
|
||||
" Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора."
|
||||
)
|
||||
elif code == "not-removed":
|
||||
state = f"Состояние: объект «{rp}» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора."
|
||||
fix = (
|
||||
"Либо сначала снять объект с поддержки, затем удалять:\n"
|
||||
f' support-edit -Path "{rp}" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно.'
|
||||
)
|
||||
else:
|
||||
state = f"Состояние: объект «{rp}» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)."
|
||||
fix = (
|
||||
"Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):\n"
|
||||
f' support-edit -Path "{rp}" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);\n'
|
||||
f' support-edit -Path "{rp}" -Set off-support — снять с поддержки: обновления по объекту больше не приходят.'
|
||||
)
|
||||
sys.stderr.write(head + "\n" + state + "\n" + cfe + "\n" + fix + "\n" + off_note + "\n")
|
||||
sys.exit(1)
|
||||
except SystemExit:
|
||||
raise
|
||||
except Exception:
|
||||
return
|
||||
|
||||
|
||||
def esc_xml(s):
|
||||
return s.replace('&', '&').replace('<', '<').replace('>', '>').replace('"', '"')
|
||||
@@ -616,6 +777,8 @@ def main():
|
||||
if not os.path.isabs(out_path):
|
||||
out_path = os.path.join(os.getcwd(), out_path)
|
||||
|
||||
assert_edit_allowed(out_path, "editable")
|
||||
|
||||
out_dir = os.path.dirname(out_path)
|
||||
if out_dir and not os.path.exists(out_dir):
|
||||
os.makedirs(out_dir, exist_ok=True)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# mxl-info v1.0 — Analyze 1C spreadsheet structure
|
||||
# mxl-info v1.1 — Analyze 1C spreadsheet structure
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Alias('Path')]
|
||||
@@ -321,11 +321,71 @@ if ($Format -eq "json") {
|
||||
exit 0
|
||||
}
|
||||
|
||||
function Get-SupportStatusForPath([string]$targetPath) {
|
||||
try {
|
||||
$rp = (Resolve-Path $targetPath).Path
|
||||
$elemUuid = $null
|
||||
$binPath = $null
|
||||
# Reads the uuid of the first metadata element in an .xml file (or $null).
|
||||
function Get-RootUuid([string]$xmlPath) {
|
||||
if (-not (Test-Path $xmlPath)) { return $null }
|
||||
try {
|
||||
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
|
||||
$el = $mx.DocumentElement.FirstChild
|
||||
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
|
||||
if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } }
|
||||
} catch {}
|
||||
return $null
|
||||
}
|
||||
# The target file itself may be the element meta-xml (e.g. Subsystems/X.xml).
|
||||
$elemUuid = Get-RootUuid $rp
|
||||
$d = [System.IO.Path]::GetDirectoryName($rp)
|
||||
for ($i = 0; $i -lt 12 -and $d; $i++) {
|
||||
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
|
||||
if (-not $binPath) {
|
||||
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
|
||||
if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $binPath = $cand }
|
||||
}
|
||||
if ($elemUuid -and $binPath) { break }
|
||||
$parent = [System.IO.Path]::GetDirectoryName($d)
|
||||
if ($parent -eq $d) { break }
|
||||
$d = $parent
|
||||
}
|
||||
if (-not $binPath -or -not (Test-Path $binPath)) { return "не на поддержке" }
|
||||
$bytes = [System.IO.File]::ReadAllBytes($binPath)
|
||||
if ($bytes.Length -le 32) { return "снято с поддержки (правки свободны)" }
|
||||
$start = 0
|
||||
if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 }
|
||||
$text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start)
|
||||
$h = [regex]::Match($text, '^\{6,(\d+),(\d+),')
|
||||
if (-not $h.Success) { return "не на поддержке" }
|
||||
$G = [int]$h.Groups[1].Value
|
||||
$K = [int]$h.Groups[2].Value
|
||||
if ($K -eq 0) { return "снято с поддержки (правки свободны)" }
|
||||
if ($G -eq 1) { return "конфигурация read-only (возможность изменения выключена) — правки невозможны без включения" }
|
||||
if (-not $elemUuid) { return "не на поддержке" }
|
||||
$u = [regex]::Escape($elemUuid.ToLower())
|
||||
$best = $null
|
||||
foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) {
|
||||
$f1 = [int]$m.Groups[1].Value
|
||||
if ($null -eq $best -or $f1 -lt $best) { $best = $f1 }
|
||||
}
|
||||
if ($null -eq $best) { return "не на поддержке" }
|
||||
switch ($best) {
|
||||
0 { return "на замке — прямая правка сломает обновления; дорабатывай через cfe-* либо включи редактирование объекта" }
|
||||
1 { return "редактируется с сохранением поддержки" }
|
||||
2 { return "снято с поддержки (правки свободны)" }
|
||||
}
|
||||
return "не на поддержке"
|
||||
} catch { return "не на поддержке" }
|
||||
}
|
||||
|
||||
# --- Text format output ---
|
||||
|
||||
$lines = @()
|
||||
|
||||
$lines += "=== $templateName ==="
|
||||
$lines += "Поддержка: $(Get-SupportStatusForPath $TemplatePath)"
|
||||
$lines += " Rows: $docHeight, Columns: $defaultColCount"
|
||||
|
||||
if ($columnSets.Count -eq 0) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# mxl-info v1.0 — Analyze 1C spreadsheet structure
|
||||
# mxl-info v1.1 — Analyze 1C spreadsheet structure
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
@@ -308,10 +308,79 @@ if args.Format == "json":
|
||||
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||||
sys.exit(0)
|
||||
|
||||
def get_support_status_for_path(target_path):
|
||||
try:
|
||||
def root_uuid(xml_path):
|
||||
if not os.path.isfile(xml_path):
|
||||
return None
|
||||
try:
|
||||
mx = etree.parse(xml_path).getroot()
|
||||
for child in mx:
|
||||
if isinstance(child.tag, str) and child.get("uuid"):
|
||||
return child.get("uuid")
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
rp = os.path.abspath(target_path)
|
||||
# The target file itself may be the element meta-xml (e.g. Subsystems/X.xml).
|
||||
elem_uuid = root_uuid(rp)
|
||||
bin_path = None
|
||||
d = os.path.dirname(rp)
|
||||
for _ in range(12):
|
||||
if not d:
|
||||
break
|
||||
if not elem_uuid:
|
||||
elem_uuid = root_uuid(d + ".xml")
|
||||
if not bin_path:
|
||||
cand = os.path.join(d, "Ext", "ParentConfigurations.bin")
|
||||
if os.path.exists(cand) or os.path.exists(os.path.join(d, "Configuration.xml")):
|
||||
bin_path = cand
|
||||
if elem_uuid and bin_path:
|
||||
break
|
||||
parent = os.path.dirname(d)
|
||||
if parent == d:
|
||||
break
|
||||
d = parent
|
||||
if not bin_path or not os.path.exists(bin_path):
|
||||
return "не на поддержке"
|
||||
data = open(bin_path, "rb").read()
|
||||
if len(data) <= 32:
|
||||
return "снято с поддержки (правки свободны)"
|
||||
if data[:3] == b"\xef\xbb\xbf":
|
||||
data = data[3:]
|
||||
text = data.decode("utf-8", "replace")
|
||||
h = re.match(r"\{6,(\d+),(\d+),", text)
|
||||
if not h:
|
||||
return "не на поддержке"
|
||||
g = int(h.group(1))
|
||||
k = int(h.group(2))
|
||||
if k == 0:
|
||||
return "снято с поддержки (правки свободны)"
|
||||
if g == 1:
|
||||
return "конфигурация read-only (возможность изменения выключена) — правки невозможны без включения"
|
||||
if not elem_uuid:
|
||||
return "не на поддержке"
|
||||
best = None
|
||||
for m in re.finditer(r"([0-2]),0," + re.escape(elem_uuid.lower()), text):
|
||||
f1 = int(m.group(1))
|
||||
if best is None or f1 < best:
|
||||
best = f1
|
||||
if best is None:
|
||||
return "не на поддержке"
|
||||
return {
|
||||
0: "на замке — прямая правка сломает обновления; дорабатывай через cfe-* либо включи редактирование объекта",
|
||||
1: "редактируется с сохранением поддержки",
|
||||
2: "снято с поддержки (правки свободны)",
|
||||
}.get(best, "не на поддержке")
|
||||
except Exception:
|
||||
return "не на поддержке"
|
||||
|
||||
|
||||
# --- Text format output ---
|
||||
lines = []
|
||||
|
||||
lines.append(f"=== {template_name} ===")
|
||||
lines.append(f"Поддержка: {get_support_status_for_path(template_path)}")
|
||||
lines.append(f" Rows: {doc_height}, Columns: {default_col_count}")
|
||||
|
||||
if len(column_sets) == 0:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# role-compile v1.5 — Compile 1C role from JSON
|
||||
# role-compile v1.7 — Compile 1C role from JSON
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
@@ -11,6 +11,124 @@ param(
|
||||
$ErrorActionPreference = "Stop"
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# --- Support guard (Ext/ParentConfigurations.bin) ---
|
||||
# See docs/1c-support-state-spec.md. Blocks edits of vendor objects "на замке" /
|
||||
# read-only configs unless allowed. Trigger = bin present; reaction from
|
||||
# .v8-project.json editingAllowedCheck (deny|warn|off, default deny). Never
|
||||
# throws — guard errors degrade to allow.
|
||||
function Get-RootUuid([string]$xmlPath) {
|
||||
if (-not (Test-Path $xmlPath)) { return $null }
|
||||
try {
|
||||
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
|
||||
$el = $mx.DocumentElement.FirstChild
|
||||
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
|
||||
if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } }
|
||||
} catch {}
|
||||
return $null
|
||||
}
|
||||
function Find-V8Project([string]$startDir) {
|
||||
$d = $startDir
|
||||
for ($i = 0; $i -lt 20 -and $d; $i++) {
|
||||
$pj = Join-Path $d ".v8-project.json"
|
||||
if (Test-Path $pj) { return $pj }
|
||||
$parent = [System.IO.Path]::GetDirectoryName($d)
|
||||
if ($parent -eq $d) { break }
|
||||
$d = $parent
|
||||
}
|
||||
return $null
|
||||
}
|
||||
function Get-EditMode([string]$cfgDir) {
|
||||
try {
|
||||
$pj = Find-V8Project (Get-Location).Path
|
||||
if (-not $pj) { $pj = Find-V8Project $cfgDir }
|
||||
if (-not $pj) { return 'deny' }
|
||||
$proj = Get-Content -Raw $pj | ConvertFrom-Json
|
||||
$cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/')
|
||||
if ($proj.databases) {
|
||||
foreach ($db in $proj.databases) {
|
||||
if ($db.configSrc) {
|
||||
$src = [System.IO.Path]::GetFullPath($db.configSrc).TrimEnd('\', '/')
|
||||
if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) {
|
||||
if ($db.editingAllowedCheck) { return $db.editingAllowedCheck }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($proj.editingAllowedCheck) { return $proj.editingAllowedCheck }
|
||||
return 'deny'
|
||||
} catch { return 'deny' }
|
||||
}
|
||||
function Assert-EditAllowed([string]$targetPath, [string]$require) {
|
||||
try {
|
||||
$rp = $targetPath
|
||||
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
|
||||
$elemUuid = Get-RootUuid $rp
|
||||
$cfgDir = $null; $binPath = $null
|
||||
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
|
||||
for ($i = 0; $i -lt 12 -and $d; $i++) {
|
||||
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
|
||||
if (-not $cfgDir) {
|
||||
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
|
||||
if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $cfgDir = $d; $binPath = $cand }
|
||||
}
|
||||
if ($elemUuid -and $cfgDir) { break }
|
||||
$parent = [System.IO.Path]::GetDirectoryName($d)
|
||||
if ($parent -eq $d) { break }
|
||||
$d = $parent
|
||||
}
|
||||
# New object (no element file): fall back to config root uuid.
|
||||
if (-not $elemUuid -and $cfgDir) { $elemUuid = Get-RootUuid (Join-Path $cfgDir "Configuration.xml") }
|
||||
if (-not $binPath -or -not (Test-Path $binPath)) { return }
|
||||
$bytes = [System.IO.File]::ReadAllBytes($binPath)
|
||||
if ($bytes.Length -le 32) { return }
|
||||
$start = 0
|
||||
if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 }
|
||||
$text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start)
|
||||
$hm = [regex]::Match($text, '^\{6,(\d+),(\d+),')
|
||||
if (-not $hm.Success) { return }
|
||||
$G = [int]$hm.Groups[1].Value
|
||||
$K = [int]$hm.Groups[2].Value
|
||||
if ($K -eq 0) { return }
|
||||
$best = $null
|
||||
if ($elemUuid) {
|
||||
$u = [regex]::Escape($elemUuid.ToLower())
|
||||
foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) {
|
||||
$f1 = [int]$m.Groups[1].Value
|
||||
if ($null -eq $best -or $f1 -lt $best) { $best = $f1 }
|
||||
}
|
||||
}
|
||||
$blocked = $false; $code = ""; $reason = ""
|
||||
if ($G -eq 1) { $blocked = $true; $code = "capability-off"; $reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)" }
|
||||
elseif ($require -eq 'removed') {
|
||||
if ($null -ne $best -and $best -ne 2) { $blocked = $true; $code = "not-removed"; $reason = "объект не снят с поддержки — удаление сломает обновления" }
|
||||
}
|
||||
else {
|
||||
if ($null -ne $best -and $best -eq 0) { $blocked = $true; $code = "locked"; $reason = "объект на замке — редактирование сломает обновления" }
|
||||
}
|
||||
if (-not $blocked) { return }
|
||||
$mode = Get-EditMode $cfgDir
|
||||
if ($mode -eq 'off') { return }
|
||||
# Use Console.Error (not Write-Error) — under ErrorActionPreference=Stop the
|
||||
# latter throws and would be swallowed by this function's own catch.
|
||||
if ($mode -eq 'warn') { [Console]::Error.WriteLine("[support-guard] ПРЕДУПРЕЖДЕНИЕ: $reason. Цель: $rp"); return }
|
||||
$head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления."
|
||||
$cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются."
|
||||
$offNote = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json."
|
||||
if ($code -eq "capability-off") {
|
||||
$state = "Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «$rp» редактировать нельзя."
|
||||
$fix = "Либо снять защиту явно (навык support-edit, два шага):`n 1. support-edit -Path ""$cfgDir"" -Capability on — включить возможность изменения (объекты пока остаются на замке);`n 2. support-edit -Path ""$rp"" -Set editable — открыть этот объект для редактирования.`n Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора."
|
||||
} elseif ($code -eq "not-removed") {
|
||||
$state = "Состояние: объект «$rp» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора."
|
||||
$fix = "Либо сначала снять объект с поддержки, затем удалять:`n support-edit -Path ""$rp"" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно."
|
||||
} else {
|
||||
$state = "Состояние: объект «$rp» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)."
|
||||
$fix = "Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):`n support-edit -Path ""$rp"" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);`n support-edit -Path ""$rp"" -Set off-support — снять с поддержки: обновления по объекту больше не приходят."
|
||||
}
|
||||
[Console]::Error.WriteLine("$head`n$state`n$cfe`n$fix`n$offNote")
|
||||
exit 1
|
||||
} catch { return }
|
||||
}
|
||||
|
||||
# --- 1. Load and validate JSON ---
|
||||
|
||||
if (-not (Test-Path $JsonPath)) {
|
||||
@@ -523,6 +641,7 @@ function Detect-FormatVersion([string]$dir) {
|
||||
}
|
||||
|
||||
$resolvedOutputDir = if ([System.IO.Path]::IsPathRooted($OutputDir)) { $OutputDir } else { Join-Path (Get-Location) $OutputDir }
|
||||
Assert-EditAllowed $resolvedOutputDir 'editable'
|
||||
$formatVersion = Detect-FormatVersion $resolvedOutputDir
|
||||
|
||||
# --- 8. Generate UUID ---
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# role-compile v1.4 — Compile 1C role from JSON
|
||||
# role-compile v1.7 — Compile 1C role from JSON
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
import argparse
|
||||
import json
|
||||
@@ -8,6 +8,167 @@ import re
|
||||
import sys
|
||||
import uuid
|
||||
|
||||
from lxml import etree
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md
|
||||
# Blocks edits of vendor objects "на замке" / read-only configs. Trigger = bin
|
||||
# present; reaction from .v8-project.json editingAllowedCheck (deny|warn|off,
|
||||
# default deny). Never throws (except sys.exit on deny) — errors degrade to allow.
|
||||
# ============================================================
|
||||
|
||||
def _sg_root_uuid(xml_path):
|
||||
if not os.path.isfile(xml_path):
|
||||
return None
|
||||
try:
|
||||
mx = etree.parse(xml_path).getroot()
|
||||
for child in mx:
|
||||
if isinstance(child.tag, str) and child.get("uuid"):
|
||||
return child.get("uuid")
|
||||
except Exception:
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def _sg_find_v8project(start_dir):
|
||||
d = start_dir
|
||||
for _ in range(20):
|
||||
if not d:
|
||||
break
|
||||
pj = os.path.join(d, ".v8-project.json")
|
||||
if os.path.isfile(pj):
|
||||
return pj
|
||||
parent = os.path.dirname(d)
|
||||
if parent == d:
|
||||
break
|
||||
d = parent
|
||||
return None
|
||||
|
||||
|
||||
def _sg_get_edit_mode(cfg_dir):
|
||||
try:
|
||||
pj = _sg_find_v8project(os.getcwd()) or _sg_find_v8project(cfg_dir)
|
||||
if not pj:
|
||||
return "deny"
|
||||
proj = json.loads(open(pj, encoding="utf-8-sig").read())
|
||||
cfg_full = os.path.normcase(os.path.abspath(cfg_dir)).rstrip("\\/")
|
||||
for db in proj.get("databases", []):
|
||||
src = db.get("configSrc")
|
||||
if src:
|
||||
src_full = os.path.normcase(os.path.abspath(src)).rstrip("\\/")
|
||||
if cfg_full == src_full or cfg_full.startswith(src_full + os.sep):
|
||||
if db.get("editingAllowedCheck"):
|
||||
return db["editingAllowedCheck"]
|
||||
if proj.get("editingAllowedCheck"):
|
||||
return proj["editingAllowedCheck"]
|
||||
return "deny"
|
||||
except Exception:
|
||||
return "deny"
|
||||
|
||||
|
||||
def assert_edit_allowed(target_path, require):
|
||||
try:
|
||||
rp = os.path.abspath(target_path)
|
||||
elem_uuid = _sg_root_uuid(rp)
|
||||
cfg_dir = None
|
||||
bin_path = None
|
||||
d = rp if os.path.isdir(rp) else os.path.dirname(rp)
|
||||
for _ in range(12):
|
||||
if not d:
|
||||
break
|
||||
if not elem_uuid:
|
||||
elem_uuid = _sg_root_uuid(d + ".xml")
|
||||
if not cfg_dir:
|
||||
cand = os.path.join(d, "Ext", "ParentConfigurations.bin")
|
||||
if os.path.exists(cand) or os.path.exists(os.path.join(d, "Configuration.xml")):
|
||||
cfg_dir = d
|
||||
bin_path = cand
|
||||
if elem_uuid and cfg_dir:
|
||||
break
|
||||
parent = os.path.dirname(d)
|
||||
if parent == d:
|
||||
break
|
||||
d = parent
|
||||
if not elem_uuid and cfg_dir:
|
||||
elem_uuid = _sg_root_uuid(os.path.join(cfg_dir, "Configuration.xml"))
|
||||
if not bin_path or not os.path.exists(bin_path):
|
||||
return
|
||||
data = open(bin_path, "rb").read()
|
||||
if len(data) <= 32:
|
||||
return
|
||||
if data[:3] == b"\xef\xbb\xbf":
|
||||
data = data[3:]
|
||||
text = data.decode("utf-8", "replace")
|
||||
h = re.match(r"\{6,(\d+),(\d+),", text)
|
||||
if not h:
|
||||
return
|
||||
g = int(h.group(1))
|
||||
k = int(h.group(2))
|
||||
if k == 0:
|
||||
return
|
||||
best = None
|
||||
if elem_uuid:
|
||||
for m in re.finditer(r"([0-2]),0," + re.escape(elem_uuid.lower()), text):
|
||||
f1 = int(m.group(1))
|
||||
if best is None or f1 < best:
|
||||
best = f1
|
||||
blocked = False
|
||||
code = ""
|
||||
reason = ""
|
||||
if g == 1:
|
||||
blocked = True
|
||||
code = "capability-off"
|
||||
reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)"
|
||||
elif require == "removed":
|
||||
if best is not None and best != 2:
|
||||
blocked = True
|
||||
code = "not-removed"
|
||||
reason = "объект не снят с поддержки — удаление сломает обновления"
|
||||
else:
|
||||
if best is not None and best == 0:
|
||||
blocked = True
|
||||
code = "locked"
|
||||
reason = "объект на замке — редактирование сломает обновления"
|
||||
if not blocked:
|
||||
return
|
||||
mode = _sg_get_edit_mode(cfg_dir)
|
||||
if mode == "off":
|
||||
return
|
||||
if mode == "warn":
|
||||
sys.stderr.write(f"[support-guard] ПРЕДУПРЕЖДЕНИЕ: {reason}. Цель: {rp}\n")
|
||||
return
|
||||
head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления."
|
||||
cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются."
|
||||
off_note = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json."
|
||||
if code == "capability-off":
|
||||
state = f"Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «{rp}» редактировать нельзя."
|
||||
fix = (
|
||||
"Либо снять защиту явно (навык support-edit, два шага):\n"
|
||||
f' 1. support-edit -Path "{cfg_dir}" -Capability on — включить возможность изменения (объекты пока остаются на замке);\n'
|
||||
f' 2. support-edit -Path "{rp}" -Set editable — открыть этот объект для редактирования.\n'
|
||||
" Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора."
|
||||
)
|
||||
elif code == "not-removed":
|
||||
state = f"Состояние: объект «{rp}» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора."
|
||||
fix = (
|
||||
"Либо сначала снять объект с поддержки, затем удалять:\n"
|
||||
f' support-edit -Path "{rp}" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно.'
|
||||
)
|
||||
else:
|
||||
state = f"Состояние: объект «{rp}» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)."
|
||||
fix = (
|
||||
"Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):\n"
|
||||
f' support-edit -Path "{rp}" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);\n'
|
||||
f' support-edit -Path "{rp}" -Set off-support — снять с поддержки: обновления по объекту больше не приходят.'
|
||||
)
|
||||
sys.stderr.write(head + "\n" + state + "\n" + cfe + "\n" + fix + "\n" + off_note + "\n")
|
||||
sys.exit(1)
|
||||
except SystemExit:
|
||||
raise
|
||||
except Exception:
|
||||
return
|
||||
|
||||
|
||||
def detect_format_version(d):
|
||||
while d:
|
||||
@@ -476,6 +637,7 @@ def main():
|
||||
defn['objects'] = defn['rights']
|
||||
|
||||
out_dir_resolved = args.OutputDir if os.path.isabs(args.OutputDir) else os.path.join(os.getcwd(), args.OutputDir)
|
||||
assert_edit_allowed(out_dir_resolved, "editable")
|
||||
format_version = detect_format_version(out_dir_resolved)
|
||||
|
||||
# --- 2. Parse all object entries ---
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# role-info v1.0 — Analyze 1C role rights
|
||||
# role-info v1.1 — Analyze 1C role rights
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][Alias('Path')][string]$RightsPath,
|
||||
@@ -145,11 +145,71 @@ foreach ($tpl in $tplNodes) {
|
||||
}
|
||||
}
|
||||
|
||||
function Get-SupportStatusForPath([string]$targetPath) {
|
||||
try {
|
||||
$rp = (Resolve-Path $targetPath).Path
|
||||
$elemUuid = $null
|
||||
$binPath = $null
|
||||
# Reads the uuid of the first metadata element in an .xml file (or $null).
|
||||
function Get-RootUuid([string]$xmlPath) {
|
||||
if (-not (Test-Path $xmlPath)) { return $null }
|
||||
try {
|
||||
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
|
||||
$el = $mx.DocumentElement.FirstChild
|
||||
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
|
||||
if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } }
|
||||
} catch {}
|
||||
return $null
|
||||
}
|
||||
# The target file itself may be the element meta-xml (e.g. Subsystems/X.xml).
|
||||
$elemUuid = Get-RootUuid $rp
|
||||
$d = [System.IO.Path]::GetDirectoryName($rp)
|
||||
for ($i = 0; $i -lt 12 -and $d; $i++) {
|
||||
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
|
||||
if (-not $binPath) {
|
||||
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
|
||||
if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $binPath = $cand }
|
||||
}
|
||||
if ($elemUuid -and $binPath) { break }
|
||||
$parent = [System.IO.Path]::GetDirectoryName($d)
|
||||
if ($parent -eq $d) { break }
|
||||
$d = $parent
|
||||
}
|
||||
if (-not $binPath -or -not (Test-Path $binPath)) { return "не на поддержке" }
|
||||
$bytes = [System.IO.File]::ReadAllBytes($binPath)
|
||||
if ($bytes.Length -le 32) { return "снято с поддержки (правки свободны)" }
|
||||
$start = 0
|
||||
if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 }
|
||||
$text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start)
|
||||
$h = [regex]::Match($text, '^\{6,(\d+),(\d+),')
|
||||
if (-not $h.Success) { return "не на поддержке" }
|
||||
$G = [int]$h.Groups[1].Value
|
||||
$K = [int]$h.Groups[2].Value
|
||||
if ($K -eq 0) { return "снято с поддержки (правки свободны)" }
|
||||
if ($G -eq 1) { return "конфигурация read-only (возможность изменения выключена) — правки невозможны без включения" }
|
||||
if (-not $elemUuid) { return "не на поддержке" }
|
||||
$u = [regex]::Escape($elemUuid.ToLower())
|
||||
$best = $null
|
||||
foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) {
|
||||
$f1 = [int]$m.Groups[1].Value
|
||||
if ($null -eq $best -or $f1 -lt $best) { $best = $f1 }
|
||||
}
|
||||
if ($null -eq $best) { return "не на поддержке" }
|
||||
switch ($best) {
|
||||
0 { return "на замке — прямая правка сломает обновления; дорабатывай через cfe-* либо включи редактирование объекта" }
|
||||
1 { return "редактируется с сохранением поддержки" }
|
||||
2 { return "снято с поддержки (правки свободны)" }
|
||||
}
|
||||
return "не на поддержке"
|
||||
} catch { return "не на поддержке" }
|
||||
}
|
||||
|
||||
# --- Output ---
|
||||
$header = "=== Role: $roleName"
|
||||
if ($roleSynonym) { $header += " --- `"$roleSynonym`"" }
|
||||
$header += " ==="
|
||||
Out $header
|
||||
Out "Поддержка: $(Get-SupportStatusForPath $RightsPath)"
|
||||
Out ""
|
||||
|
||||
Out "Properties: setForNewObjects=$setForNew, setForAttributesByDefault=$setForAttrs, independentRightsOfChildObjects=$independentChild"
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
#!/usr/bin/env python3
|
||||
# role-info v1.0 — Analyze 1C role rights
|
||||
# role-info v1.1 — Analyze 1C role rights
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from collections import OrderedDict
|
||||
from lxml import etree
|
||||
@@ -147,12 +148,81 @@ for tpl in root.findall("r:restrictionTemplate", NSMAP):
|
||||
t_name = t_name[:paren_idx]
|
||||
templates.append(t_name)
|
||||
|
||||
def get_support_status_for_path(target_path):
|
||||
try:
|
||||
def root_uuid(xml_path):
|
||||
if not os.path.isfile(xml_path):
|
||||
return None
|
||||
try:
|
||||
mx = etree.parse(xml_path).getroot()
|
||||
for child in mx:
|
||||
if isinstance(child.tag, str) and child.get("uuid"):
|
||||
return child.get("uuid")
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
rp = os.path.abspath(target_path)
|
||||
# The target file itself may be the element meta-xml (e.g. Subsystems/X.xml).
|
||||
elem_uuid = root_uuid(rp)
|
||||
bin_path = None
|
||||
d = os.path.dirname(rp)
|
||||
for _ in range(12):
|
||||
if not d:
|
||||
break
|
||||
if not elem_uuid:
|
||||
elem_uuid = root_uuid(d + ".xml")
|
||||
if not bin_path:
|
||||
cand = os.path.join(d, "Ext", "ParentConfigurations.bin")
|
||||
if os.path.exists(cand) or os.path.exists(os.path.join(d, "Configuration.xml")):
|
||||
bin_path = cand
|
||||
if elem_uuid and bin_path:
|
||||
break
|
||||
parent = os.path.dirname(d)
|
||||
if parent == d:
|
||||
break
|
||||
d = parent
|
||||
if not bin_path or not os.path.exists(bin_path):
|
||||
return "не на поддержке"
|
||||
data = open(bin_path, "rb").read()
|
||||
if len(data) <= 32:
|
||||
return "снято с поддержки (правки свободны)"
|
||||
if data[:3] == b"\xef\xbb\xbf":
|
||||
data = data[3:]
|
||||
text = data.decode("utf-8", "replace")
|
||||
h = re.match(r"\{6,(\d+),(\d+),", text)
|
||||
if not h:
|
||||
return "не на поддержке"
|
||||
g = int(h.group(1))
|
||||
k = int(h.group(2))
|
||||
if k == 0:
|
||||
return "снято с поддержки (правки свободны)"
|
||||
if g == 1:
|
||||
return "конфигурация read-only (возможность изменения выключена) — правки невозможны без включения"
|
||||
if not elem_uuid:
|
||||
return "не на поддержке"
|
||||
best = None
|
||||
for m in re.finditer(r"([0-2]),0," + re.escape(elem_uuid.lower()), text):
|
||||
f1 = int(m.group(1))
|
||||
if best is None or f1 < best:
|
||||
best = f1
|
||||
if best is None:
|
||||
return "не на поддержке"
|
||||
return {
|
||||
0: "на замке — прямая правка сломает обновления; дорабатывай через cfe-* либо включи редактирование объекта",
|
||||
1: "редактируется с сохранением поддержки",
|
||||
2: "снято с поддержки (правки свободны)",
|
||||
}.get(best, "не на поддержке")
|
||||
except Exception:
|
||||
return "не на поддержке"
|
||||
|
||||
|
||||
# --- Output ---
|
||||
header = f"=== Role: {role_name}"
|
||||
if role_synonym:
|
||||
header += f' --- "{role_synonym}"'
|
||||
header += " ==="
|
||||
out(header)
|
||||
out(f"Поддержка: {get_support_status_for_path(rights_path)}")
|
||||
out()
|
||||
|
||||
out(f"Properties: setForNewObjects={set_for_new}, setForAttributesByDefault={set_for_attrs}, independentRightsOfChildObjects={independent_child}")
|
||||
|
||||
@@ -152,11 +152,19 @@ Shorthand: `"Имя [Заголовок]: тип = значение @флаги"
|
||||
|
||||
Флаги shorthand:
|
||||
- `@autoDates` — добавляет к параметру StandardPeriod пару дат `НачалоПериода`/`КонецПериода`, вычисляемых из него. Используй их в тексте запроса как `&НачалоПериода`/`&КонецПериода`; пользователь выбирает только сам период. По умолчанию сам параметр получает `use=Always` и `denyIncompleteValues=true` (чтобы производные даты всегда были заполнены); в объектной форме можно явно переопределить.
|
||||
- `@valueList` — `<valueListAllowed>true</valueListAllowed>` — разрешает передавать список значений
|
||||
- `@valueList` — `<valueListAllowed>true</valueListAllowed>` — разрешает передавать список значений (при значении-списке ниже подразумевается автоматически)
|
||||
- `@hidden` — скрытый параметр: `availableAsField=false` + исключается из `"dataParameters": "auto"`
|
||||
|
||||
Объектная форма: `title`, `hidden: true`, `valueListAllowed: true`, `availableAsField: false`, `denyIncompleteValues: true`, `use: "Always"`.
|
||||
|
||||
Значение-список: несколько значений по умолчанию через запятую в `значение` (для запятой внутри значения — кавычки `'...'`). В объектной форме — массив в `value`.
|
||||
|
||||
```json
|
||||
"parameters": [
|
||||
"Виды: ChartOfCharacteristicTypesRef.ВидыСубконтоХозрасчетные = ПланВидовХарактеристик.ВидыСубконтоХозрасчетные.Контрагенты, ПланВидовХарактеристик.ВидыСубконтоХозрасчетные.Договоры"
|
||||
]
|
||||
```
|
||||
|
||||
Если значения по умолчанию нет — пропусти `=` в shorthand или укажи `"value": null` в объектной форме.
|
||||
|
||||
Список допустимых значений (availableValues):
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# skd-compile v1.104 — Compile 1C DCS from JSON
|
||||
# skd-compile v1.107 — Compile 1C DCS from JSON
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[string]$DefinitionFile,
|
||||
@@ -10,6 +10,124 @@ param(
|
||||
$ErrorActionPreference = "Stop"
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# --- Support guard (Ext/ParentConfigurations.bin) ---
|
||||
# See docs/1c-support-state-spec.md. Blocks edits of vendor objects "на замке" /
|
||||
# read-only configs unless allowed. Trigger = bin present; reaction from
|
||||
# .v8-project.json editingAllowedCheck (deny|warn|off, default deny). Never
|
||||
# throws — guard errors degrade to allow.
|
||||
function Get-RootUuid([string]$xmlPath) {
|
||||
if (-not (Test-Path $xmlPath)) { return $null }
|
||||
try {
|
||||
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
|
||||
$el = $mx.DocumentElement.FirstChild
|
||||
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
|
||||
if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } }
|
||||
} catch {}
|
||||
return $null
|
||||
}
|
||||
function Find-V8Project([string]$startDir) {
|
||||
$d = $startDir
|
||||
for ($i = 0; $i -lt 20 -and $d; $i++) {
|
||||
$pj = Join-Path $d ".v8-project.json"
|
||||
if (Test-Path $pj) { return $pj }
|
||||
$parent = [System.IO.Path]::GetDirectoryName($d)
|
||||
if ($parent -eq $d) { break }
|
||||
$d = $parent
|
||||
}
|
||||
return $null
|
||||
}
|
||||
function Get-EditMode([string]$cfgDir) {
|
||||
try {
|
||||
$pj = Find-V8Project (Get-Location).Path
|
||||
if (-not $pj) { $pj = Find-V8Project $cfgDir }
|
||||
if (-not $pj) { return 'deny' }
|
||||
$proj = Get-Content -Raw $pj | ConvertFrom-Json
|
||||
$cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/')
|
||||
if ($proj.databases) {
|
||||
foreach ($db in $proj.databases) {
|
||||
if ($db.configSrc) {
|
||||
$src = [System.IO.Path]::GetFullPath($db.configSrc).TrimEnd('\', '/')
|
||||
if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) {
|
||||
if ($db.editingAllowedCheck) { return $db.editingAllowedCheck }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($proj.editingAllowedCheck) { return $proj.editingAllowedCheck }
|
||||
return 'deny'
|
||||
} catch { return 'deny' }
|
||||
}
|
||||
function Assert-EditAllowed([string]$targetPath, [string]$require) {
|
||||
try {
|
||||
$rp = $targetPath
|
||||
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
|
||||
$elemUuid = Get-RootUuid $rp
|
||||
$cfgDir = $null; $binPath = $null
|
||||
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
|
||||
for ($i = 0; $i -lt 12 -and $d; $i++) {
|
||||
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
|
||||
if (-not $cfgDir) {
|
||||
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
|
||||
if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $cfgDir = $d; $binPath = $cand }
|
||||
}
|
||||
if ($elemUuid -and $cfgDir) { break }
|
||||
$parent = [System.IO.Path]::GetDirectoryName($d)
|
||||
if ($parent -eq $d) { break }
|
||||
$d = $parent
|
||||
}
|
||||
# New object (no element file): fall back to config root uuid.
|
||||
if (-not $elemUuid -and $cfgDir) { $elemUuid = Get-RootUuid (Join-Path $cfgDir "Configuration.xml") }
|
||||
if (-not $binPath -or -not (Test-Path $binPath)) { return }
|
||||
$bytes = [System.IO.File]::ReadAllBytes($binPath)
|
||||
if ($bytes.Length -le 32) { return }
|
||||
$start = 0
|
||||
if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 }
|
||||
$text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start)
|
||||
$hm = [regex]::Match($text, '^\{6,(\d+),(\d+),')
|
||||
if (-not $hm.Success) { return }
|
||||
$G = [int]$hm.Groups[1].Value
|
||||
$K = [int]$hm.Groups[2].Value
|
||||
if ($K -eq 0) { return }
|
||||
$best = $null
|
||||
if ($elemUuid) {
|
||||
$u = [regex]::Escape($elemUuid.ToLower())
|
||||
foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) {
|
||||
$f1 = [int]$m.Groups[1].Value
|
||||
if ($null -eq $best -or $f1 -lt $best) { $best = $f1 }
|
||||
}
|
||||
}
|
||||
$blocked = $false; $code = ""; $reason = ""
|
||||
if ($G -eq 1) { $blocked = $true; $code = "capability-off"; $reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)" }
|
||||
elseif ($require -eq 'removed') {
|
||||
if ($null -ne $best -and $best -ne 2) { $blocked = $true; $code = "not-removed"; $reason = "объект не снят с поддержки — удаление сломает обновления" }
|
||||
}
|
||||
else {
|
||||
if ($null -ne $best -and $best -eq 0) { $blocked = $true; $code = "locked"; $reason = "объект на замке — редактирование сломает обновления" }
|
||||
}
|
||||
if (-not $blocked) { return }
|
||||
$mode = Get-EditMode $cfgDir
|
||||
if ($mode -eq 'off') { return }
|
||||
# Use Console.Error (not Write-Error) — under ErrorActionPreference=Stop the
|
||||
# latter throws and would be swallowed by this function's own catch.
|
||||
if ($mode -eq 'warn') { [Console]::Error.WriteLine("[support-guard] ПРЕДУПРЕЖДЕНИЕ: $reason. Цель: $rp"); return }
|
||||
$head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления."
|
||||
$cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются."
|
||||
$offNote = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json."
|
||||
if ($code -eq "capability-off") {
|
||||
$state = "Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «$rp» редактировать нельзя."
|
||||
$fix = "Либо снять защиту явно (навык support-edit, два шага):`n 1. support-edit -Path ""$cfgDir"" -Capability on — включить возможность изменения (объекты пока остаются на замке);`n 2. support-edit -Path ""$rp"" -Set editable — открыть этот объект для редактирования.`n Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора."
|
||||
} elseif ($code -eq "not-removed") {
|
||||
$state = "Состояние: объект «$rp» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора."
|
||||
$fix = "Либо сначала снять объект с поддержки, затем удалять:`n support-edit -Path ""$rp"" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно."
|
||||
} else {
|
||||
$state = "Состояние: объект «$rp» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)."
|
||||
$fix = "Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):`n support-edit -Path ""$rp"" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);`n support-edit -Path ""$rp"" -Set off-support — снять с поддержки: обновления по объекту больше не приходят."
|
||||
}
|
||||
[Console]::Error.WriteLine("$head`n$state`n$cfe`n$fix`n$offNote")
|
||||
exit 1
|
||||
} catch { return }
|
||||
}
|
||||
|
||||
# --- 1. Load and validate JSON ---
|
||||
|
||||
if ($DefinitionFile -and $Value) {
|
||||
@@ -475,6 +593,31 @@ function Parse-TotalShorthand {
|
||||
|
||||
# --- 7. Parameter shorthand parser ---
|
||||
|
||||
function Split-ValueListCsv {
|
||||
# Split on top-level commas (respecting 'single'/"double" quotes), strip quotes,
|
||||
# drop empties. No ':' handling — values may contain colons (dateTime).
|
||||
param([string]$s)
|
||||
$result = @()
|
||||
if ($null -eq $s) { return ,$result }
|
||||
$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() }
|
||||
foreach ($raw in $items) {
|
||||
$t = $raw.Trim()
|
||||
if ($t.Length -ge 2 -and (($t[0] -eq "'" -and $t[-1] -eq "'") -or ($t[0] -eq '"' -and $t[-1] -eq '"'))) { $t = $t.Substring(1, $t.Length - 2) }
|
||||
if ($t -ne "") { $result += $t }
|
||||
}
|
||||
return ,$result
|
||||
}
|
||||
|
||||
function Parse-ParamShorthand {
|
||||
param([string]$s)
|
||||
|
||||
@@ -509,7 +652,17 @@ function Parse-ParamShorthand {
|
||||
$result.name = $Matches[1].Trim()
|
||||
$result.type = Resolve-TypeStr ($Matches[2].Trim())
|
||||
if ($Matches[4]) {
|
||||
$result.value = $Matches[4].Trim()
|
||||
$rhs = $Matches[4].Trim()
|
||||
$items = Split-ValueListCsv $rhs
|
||||
if ($items.Count -ge 2) {
|
||||
# Multi-value default → list; valueListAllowed implied
|
||||
$result.value = $items
|
||||
$result.valueListAllowed = $true
|
||||
} elseif ($items.Count -eq 1) {
|
||||
$result.value = $items[0]
|
||||
} else {
|
||||
$result.value = $rhs
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$result.name = $s.Trim()
|
||||
@@ -3504,6 +3657,8 @@ X '</DataCompositionSchema>'
|
||||
|
||||
# --- 13. Write output ---
|
||||
|
||||
Assert-EditAllowed $OutputPath 'editable'
|
||||
|
||||
$parentDir = [System.IO.Path]::GetDirectoryName($OutputPath)
|
||||
if ($parentDir -and -not (Test-Path $parentDir)) {
|
||||
New-Item -ItemType Directory -Force $parentDir | Out-Null
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# skd-compile v1.104 — Compile 1C DCS from JSON
|
||||
# skd-compile v1.107 — Compile 1C DCS from JSON
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
import argparse
|
||||
import json
|
||||
@@ -8,6 +8,167 @@ import re
|
||||
import sys
|
||||
import uuid
|
||||
|
||||
from lxml import etree
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md
|
||||
# Blocks edits of vendor objects "на замке" / read-only configs. Trigger = bin
|
||||
# present; reaction from .v8-project.json editingAllowedCheck (deny|warn|off,
|
||||
# default deny). Never throws (except sys.exit on deny) — errors degrade to allow.
|
||||
# ============================================================
|
||||
|
||||
def _sg_root_uuid(xml_path):
|
||||
if not os.path.isfile(xml_path):
|
||||
return None
|
||||
try:
|
||||
mx = etree.parse(xml_path).getroot()
|
||||
for child in mx:
|
||||
if isinstance(child.tag, str) and child.get("uuid"):
|
||||
return child.get("uuid")
|
||||
except Exception:
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def _sg_find_v8project(start_dir):
|
||||
d = start_dir
|
||||
for _ in range(20):
|
||||
if not d:
|
||||
break
|
||||
pj = os.path.join(d, ".v8-project.json")
|
||||
if os.path.isfile(pj):
|
||||
return pj
|
||||
parent = os.path.dirname(d)
|
||||
if parent == d:
|
||||
break
|
||||
d = parent
|
||||
return None
|
||||
|
||||
|
||||
def _sg_get_edit_mode(cfg_dir):
|
||||
try:
|
||||
pj = _sg_find_v8project(os.getcwd()) or _sg_find_v8project(cfg_dir)
|
||||
if not pj:
|
||||
return "deny"
|
||||
proj = json.loads(open(pj, encoding="utf-8-sig").read())
|
||||
cfg_full = os.path.normcase(os.path.abspath(cfg_dir)).rstrip("\\/")
|
||||
for db in proj.get("databases", []):
|
||||
src = db.get("configSrc")
|
||||
if src:
|
||||
src_full = os.path.normcase(os.path.abspath(src)).rstrip("\\/")
|
||||
if cfg_full == src_full or cfg_full.startswith(src_full + os.sep):
|
||||
if db.get("editingAllowedCheck"):
|
||||
return db["editingAllowedCheck"]
|
||||
if proj.get("editingAllowedCheck"):
|
||||
return proj["editingAllowedCheck"]
|
||||
return "deny"
|
||||
except Exception:
|
||||
return "deny"
|
||||
|
||||
|
||||
def assert_edit_allowed(target_path, require):
|
||||
try:
|
||||
rp = os.path.abspath(target_path)
|
||||
elem_uuid = _sg_root_uuid(rp)
|
||||
cfg_dir = None
|
||||
bin_path = None
|
||||
d = rp if os.path.isdir(rp) else os.path.dirname(rp)
|
||||
for _ in range(12):
|
||||
if not d:
|
||||
break
|
||||
if not elem_uuid:
|
||||
elem_uuid = _sg_root_uuid(d + ".xml")
|
||||
if not cfg_dir:
|
||||
cand = os.path.join(d, "Ext", "ParentConfigurations.bin")
|
||||
if os.path.exists(cand) or os.path.exists(os.path.join(d, "Configuration.xml")):
|
||||
cfg_dir = d
|
||||
bin_path = cand
|
||||
if elem_uuid and cfg_dir:
|
||||
break
|
||||
parent = os.path.dirname(d)
|
||||
if parent == d:
|
||||
break
|
||||
d = parent
|
||||
if not elem_uuid and cfg_dir:
|
||||
elem_uuid = _sg_root_uuid(os.path.join(cfg_dir, "Configuration.xml"))
|
||||
if not bin_path or not os.path.exists(bin_path):
|
||||
return
|
||||
data = open(bin_path, "rb").read()
|
||||
if len(data) <= 32:
|
||||
return
|
||||
if data[:3] == b"\xef\xbb\xbf":
|
||||
data = data[3:]
|
||||
text = data.decode("utf-8", "replace")
|
||||
h = re.match(r"\{6,(\d+),(\d+),", text)
|
||||
if not h:
|
||||
return
|
||||
g = int(h.group(1))
|
||||
k = int(h.group(2))
|
||||
if k == 0:
|
||||
return
|
||||
best = None
|
||||
if elem_uuid:
|
||||
for m in re.finditer(r"([0-2]),0," + re.escape(elem_uuid.lower()), text):
|
||||
f1 = int(m.group(1))
|
||||
if best is None or f1 < best:
|
||||
best = f1
|
||||
blocked = False
|
||||
code = ""
|
||||
reason = ""
|
||||
if g == 1:
|
||||
blocked = True
|
||||
code = "capability-off"
|
||||
reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)"
|
||||
elif require == "removed":
|
||||
if best is not None and best != 2:
|
||||
blocked = True
|
||||
code = "not-removed"
|
||||
reason = "объект не снят с поддержки — удаление сломает обновления"
|
||||
else:
|
||||
if best is not None and best == 0:
|
||||
blocked = True
|
||||
code = "locked"
|
||||
reason = "объект на замке — редактирование сломает обновления"
|
||||
if not blocked:
|
||||
return
|
||||
mode = _sg_get_edit_mode(cfg_dir)
|
||||
if mode == "off":
|
||||
return
|
||||
if mode == "warn":
|
||||
sys.stderr.write(f"[support-guard] ПРЕДУПРЕЖДЕНИЕ: {reason}. Цель: {rp}\n")
|
||||
return
|
||||
head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления."
|
||||
cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются."
|
||||
off_note = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json."
|
||||
if code == "capability-off":
|
||||
state = f"Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «{rp}» редактировать нельзя."
|
||||
fix = (
|
||||
"Либо снять защиту явно (навык support-edit, два шага):\n"
|
||||
f' 1. support-edit -Path "{cfg_dir}" -Capability on — включить возможность изменения (объекты пока остаются на замке);\n'
|
||||
f' 2. support-edit -Path "{rp}" -Set editable — открыть этот объект для редактирования.\n'
|
||||
" Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора."
|
||||
)
|
||||
elif code == "not-removed":
|
||||
state = f"Состояние: объект «{rp}» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора."
|
||||
fix = (
|
||||
"Либо сначала снять объект с поддержки, затем удалять:\n"
|
||||
f' support-edit -Path "{rp}" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно.'
|
||||
)
|
||||
else:
|
||||
state = f"Состояние: объект «{rp}» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)."
|
||||
fix = (
|
||||
"Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):\n"
|
||||
f' support-edit -Path "{rp}" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);\n'
|
||||
f' support-edit -Path "{rp}" -Set off-support — снять с поддержки: обновления по объекту больше не приходят.'
|
||||
)
|
||||
sys.stderr.write(head + "\n" + state + "\n" + cfe + "\n" + fix + "\n" + off_note + "\n")
|
||||
sys.exit(1)
|
||||
except SystemExit:
|
||||
raise
|
||||
except Exception:
|
||||
return
|
||||
|
||||
|
||||
def esc_xml(s):
|
||||
return s.replace('&', '&').replace('<', '<').replace('>', '>')
|
||||
@@ -325,6 +486,39 @@ def parse_total_shorthand(s):
|
||||
|
||||
# --- Parameter shorthand parser ---
|
||||
|
||||
def split_value_list_csv(s):
|
||||
"""Split on top-level commas (respecting single/double quotes), strip quotes,
|
||||
drop empties. No ':' handling — values may contain colons (dateTime)."""
|
||||
result = []
|
||||
if s is None:
|
||||
return result
|
||||
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))
|
||||
for raw in items:
|
||||
t = raw.strip()
|
||||
if len(t) >= 2 and ((t[0] == "'" and t[-1] == "'") or (t[0] == '"' and t[-1] == '"')):
|
||||
t = t[1:-1]
|
||||
if t != "":
|
||||
result.append(t)
|
||||
return result
|
||||
|
||||
|
||||
def parse_param_shorthand(s):
|
||||
result = {'name': '', 'type': '', 'value': None, 'autoDates': False, 'title': None}
|
||||
|
||||
@@ -355,7 +549,16 @@ def parse_param_shorthand(s):
|
||||
result['name'] = m.group(1).strip()
|
||||
result['type'] = resolve_type_str(m.group(2).strip())
|
||||
if m.group(4):
|
||||
result['value'] = m.group(4).strip()
|
||||
rhs = m.group(4).strip()
|
||||
items = split_value_list_csv(rhs)
|
||||
if len(items) >= 2:
|
||||
# Multi-value default → list; valueListAllowed implied
|
||||
result['value'] = items
|
||||
result['valueListAllowed'] = True
|
||||
elif len(items) == 1:
|
||||
result['value'] = items[0]
|
||||
else:
|
||||
result['value'] = rhs
|
||||
else:
|
||||
result['name'] = s.strip()
|
||||
|
||||
@@ -2836,6 +3039,8 @@ def main():
|
||||
if not os.path.isabs(output_path):
|
||||
output_path = os.path.join(os.getcwd(), output_path)
|
||||
|
||||
assert_edit_allowed(output_path, "editable")
|
||||
|
||||
parent_dir = os.path.dirname(output_path)
|
||||
if parent_dir and not os.path.exists(parent_dir):
|
||||
os.makedirs(parent_dir, exist_ok=True)
|
||||
|
||||
@@ -91,6 +91,13 @@ Shorthand: `"Имя [Заголовок]: тип = значение [availableVa
|
||||
- `@autoDates` — генерирует пару скрытых параметров `ДатаНачала`/`ДатаОкончания` для StandardPeriod-параметра.
|
||||
- `@hidden` — скрывает параметр от пользовательских настроек (для параметров-констант, используемых в запросе).
|
||||
- `@always` — параметр всегда подставляется в запрос. Часто вместе с `@hidden`, но используется и отдельно (для видимых обязательных параметров типа отчётного периода).
|
||||
- `@valueList` — разрешает передавать в параметр список значений (при значении-списке ниже подразумевается автоматически, отдельно указывать не обязательно).
|
||||
|
||||
Значение-список: несколько значений по умолчанию задаются через запятую в `значение`. Для запятой внутри одного значения — кавычки `'...'`.
|
||||
|
||||
```
|
||||
"Виды [Виды субконто]: ChartOfCharacteristicTypesRef.ВидыСубконтоХозрасчетные = ПланВидовХарактеристик.ВидыСубконтоХозрасчетные.Контрагенты, ПланВидовХарактеристик.ВидыСубконтоХозрасчетные.Договоры"
|
||||
```
|
||||
|
||||
```
|
||||
"ПС: CatalogRef.Контрагенты = Справочник.Контрагенты.ПустаяСсылка @hidden"
|
||||
@@ -115,6 +122,7 @@ Shorthand: `"ИмяПараметра [Заголовок] [ключ=значе
|
||||
"ПериодОтчета [Отчетный период]" # только title
|
||||
"ПорядокОкругления availableValue=Перечисление.Округления.Окр1: руб., Перечисление.Округления.Окр1000: тыс."
|
||||
"СчетПС value=ПланСчетов.Хозрасчетный.КассаПредприятия"
|
||||
"Виды value=ПланВидовХарактеристик.ВидыСубконтоХозрасчетные.Контрагенты, ПланВидовХарактеристик.ВидыСубконтоХозрасчетные.Договоры"
|
||||
"Контрагент @hidden @always"
|
||||
```
|
||||
|
||||
@@ -122,7 +130,7 @@ Shorthand: `"ИмяПараметра [Заголовок] [ключ=значе
|
||||
|
||||
`availableValue=` **заменяет весь список** допустимых значений (старые удаляются). Формат и кавычки — те же, что в `add-parameter`.
|
||||
|
||||
`value=` заменяет значение параметра (тип значения подбирается автоматически по объявленному типу параметра).
|
||||
`value=` заменяет значение параметра. Несколько значений через запятую → **список значений** (заменяет все прежние); для запятой внутри значения — кавычки `'...'`.
|
||||
|
||||
Флаги `@hidden` / `@always` — те же, что и в `add-parameter`. Идемпотентны.
|
||||
|
||||
@@ -256,6 +264,8 @@ Value — имена ресурсов (как в полях/вычисляемы
|
||||
|
||||
Не поддерживает пакетный режим. Value — полный текст запроса или `@path/to/file.sql` (ссылка на внешний файл). Путь разрешается относительно Template.xml, затем CWD.
|
||||
|
||||
Когда что: **существенная переработка** (добавить поля, соединения, переписать пакет) → выгрузи запрос через `/skd-info <tpl> -Mode query -Name <набор> -Raw -OutFile file.sql`, отредактируй файл, верни `set-query @file`. `-Raw` отдаёт запрос целиком без декораций, поэтому выгрузка ↔ возврат точны (включая многопакетные запросы). **Точечная замена** (переименовать идентификатор, заменить подстроку) → выгрузка не нужна, используй `patch-query` ниже.
|
||||
|
||||
### patch-query — точечная замена в тексте запроса
|
||||
|
||||
Shorthand: `"старое => новое [@once]"`. По умолчанию заменяет все вхождения подстроки. Поддерживает пакетный режим и `-DataSet`.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# skd-edit v1.24 — Atomic 1C DCS editor
|
||||
# skd-edit v1.27 — Atomic 1C DCS editor
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
@@ -48,6 +48,126 @@ if (-not (Test-Path $TemplatePath)) {
|
||||
|
||||
$resolvedPath = (Resolve-Path $TemplatePath).Path
|
||||
|
||||
# --- Support guard (Ext/ParentConfigurations.bin) ---
|
||||
# See docs/1c-support-state-spec.md. Blocks edits of vendor objects "на замке" /
|
||||
# read-only configs unless allowed. Trigger = bin present; reaction from
|
||||
# .v8-project.json editingAllowedCheck (deny|warn|off, default deny). Never
|
||||
# throws — guard errors degrade to allow.
|
||||
function Get-RootUuid([string]$xmlPath) {
|
||||
if (-not (Test-Path $xmlPath)) { return $null }
|
||||
try {
|
||||
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
|
||||
$el = $mx.DocumentElement.FirstChild
|
||||
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
|
||||
if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } }
|
||||
} catch {}
|
||||
return $null
|
||||
}
|
||||
function Find-V8Project([string]$startDir) {
|
||||
$d = $startDir
|
||||
for ($i = 0; $i -lt 20 -and $d; $i++) {
|
||||
$pj = Join-Path $d ".v8-project.json"
|
||||
if (Test-Path $pj) { return $pj }
|
||||
$parent = [System.IO.Path]::GetDirectoryName($d)
|
||||
if ($parent -eq $d) { break }
|
||||
$d = $parent
|
||||
}
|
||||
return $null
|
||||
}
|
||||
function Get-EditMode([string]$cfgDir) {
|
||||
try {
|
||||
$pj = Find-V8Project (Get-Location).Path
|
||||
if (-not $pj) { $pj = Find-V8Project $cfgDir }
|
||||
if (-not $pj) { return 'deny' }
|
||||
$proj = Get-Content -Raw $pj | ConvertFrom-Json
|
||||
$cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/')
|
||||
if ($proj.databases) {
|
||||
foreach ($db in $proj.databases) {
|
||||
if ($db.configSrc) {
|
||||
$src = [System.IO.Path]::GetFullPath($db.configSrc).TrimEnd('\', '/')
|
||||
if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) {
|
||||
if ($db.editingAllowedCheck) { return $db.editingAllowedCheck }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($proj.editingAllowedCheck) { return $proj.editingAllowedCheck }
|
||||
return 'deny'
|
||||
} catch { return 'deny' }
|
||||
}
|
||||
function Assert-EditAllowed([string]$targetPath, [string]$require) {
|
||||
try {
|
||||
$rp = $targetPath
|
||||
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
|
||||
$elemUuid = Get-RootUuid $rp
|
||||
$cfgDir = $null; $binPath = $null
|
||||
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
|
||||
for ($i = 0; $i -lt 12 -and $d; $i++) {
|
||||
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
|
||||
if (-not $cfgDir) {
|
||||
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
|
||||
if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $cfgDir = $d; $binPath = $cand }
|
||||
}
|
||||
if ($elemUuid -and $cfgDir) { break }
|
||||
$parent = [System.IO.Path]::GetDirectoryName($d)
|
||||
if ($parent -eq $d) { break }
|
||||
$d = $parent
|
||||
}
|
||||
# New object (no element file): fall back to config root uuid.
|
||||
if (-not $elemUuid -and $cfgDir) { $elemUuid = Get-RootUuid (Join-Path $cfgDir "Configuration.xml") }
|
||||
if (-not $binPath -or -not (Test-Path $binPath)) { return }
|
||||
$bytes = [System.IO.File]::ReadAllBytes($binPath)
|
||||
if ($bytes.Length -le 32) { return }
|
||||
$start = 0
|
||||
if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 }
|
||||
$text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start)
|
||||
$hm = [regex]::Match($text, '^\{6,(\d+),(\d+),')
|
||||
if (-not $hm.Success) { return }
|
||||
$G = [int]$hm.Groups[1].Value
|
||||
$K = [int]$hm.Groups[2].Value
|
||||
if ($K -eq 0) { return }
|
||||
$best = $null
|
||||
if ($elemUuid) {
|
||||
$u = [regex]::Escape($elemUuid.ToLower())
|
||||
foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) {
|
||||
$f1 = [int]$m.Groups[1].Value
|
||||
if ($null -eq $best -or $f1 -lt $best) { $best = $f1 }
|
||||
}
|
||||
}
|
||||
$blocked = $false; $code = ""; $reason = ""
|
||||
if ($G -eq 1) { $blocked = $true; $code = "capability-off"; $reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)" }
|
||||
elseif ($require -eq 'removed') {
|
||||
if ($null -ne $best -and $best -ne 2) { $blocked = $true; $code = "not-removed"; $reason = "объект не снят с поддержки — удаление сломает обновления" }
|
||||
}
|
||||
else {
|
||||
if ($null -ne $best -and $best -eq 0) { $blocked = $true; $code = "locked"; $reason = "объект на замке — редактирование сломает обновления" }
|
||||
}
|
||||
if (-not $blocked) { return }
|
||||
$mode = Get-EditMode $cfgDir
|
||||
if ($mode -eq 'off') { return }
|
||||
# Use Console.Error (not Write-Error) — under ErrorActionPreference=Stop the
|
||||
# latter throws and would be swallowed by this function's own catch.
|
||||
if ($mode -eq 'warn') { [Console]::Error.WriteLine("[support-guard] ПРЕДУПРЕЖДЕНИЕ: $reason. Цель: $rp"); return }
|
||||
$head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления."
|
||||
$cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются."
|
||||
$offNote = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json."
|
||||
if ($code -eq "capability-off") {
|
||||
$state = "Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «$rp» редактировать нельзя."
|
||||
$fix = "Либо снять защиту явно (навык support-edit, два шага):`n 1. support-edit -Path ""$cfgDir"" -Capability on — включить возможность изменения (объекты пока остаются на замке);`n 2. support-edit -Path ""$rp"" -Set editable — открыть этот объект для редактирования.`n Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора."
|
||||
} elseif ($code -eq "not-removed") {
|
||||
$state = "Состояние: объект «$rp» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора."
|
||||
$fix = "Либо сначала снять объект с поддержки, затем удалять:`n support-edit -Path ""$rp"" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно."
|
||||
} else {
|
||||
$state = "Состояние: объект «$rp» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)."
|
||||
$fix = "Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):`n support-edit -Path ""$rp"" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);`n support-edit -Path ""$rp"" -Set off-support — снять с поддержки: обновления по объекту больше не приходят."
|
||||
}
|
||||
[Console]::Error.WriteLine("$head`n$state`n$cfe`n$fix`n$offNote")
|
||||
exit 1
|
||||
} catch { return }
|
||||
}
|
||||
|
||||
Assert-EditAllowed $resolvedPath 'editable'
|
||||
|
||||
function Esc-Xml {
|
||||
param([string]$s)
|
||||
return $s.Replace('&','&').Replace('<','<').Replace('>','>')
|
||||
@@ -378,7 +498,19 @@ function Parse-ParamShorthand {
|
||||
$hasEq = $null -ne $Matches[3]
|
||||
$rhs = $Matches[4]
|
||||
if ($hasEq) {
|
||||
$result.value = if ($rhs) { $rhs.Trim() } else { "" }
|
||||
if ($rhs -and $rhs.Trim()) {
|
||||
$items = Parse-ValueList $rhs.Trim()
|
||||
if ($items.Count -ge 2) {
|
||||
# Multi-value default → list; valueListAllowed implied
|
||||
$result.value = $items
|
||||
$result.valueListAllowed = $true
|
||||
} else {
|
||||
# Scalar (single item, quotes stripped) or empty sentinel
|
||||
$result.value = if ($items.Count -eq 1) { $items[0] } else { "" }
|
||||
}
|
||||
} else {
|
||||
$result.value = ""
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$result.name = $s.Trim()
|
||||
@@ -679,16 +811,13 @@ 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.
|
||||
function Split-QuotedCsv {
|
||||
# Splits on top-level commas, respecting 'single' and "double" quoted spans.
|
||||
# Returns raw (un-stripped, un-trimmed) item spans. Used by both availableValue
|
||||
# (value:presentation) and value-list (values only) parsing.
|
||||
param([string]$s)
|
||||
|
||||
$result = @()
|
||||
if (-not $s) { return ,$result }
|
||||
|
||||
# Tokenize by ',' respecting quoted spans
|
||||
$items = @()
|
||||
if ($null -eq $s) { return ,$items }
|
||||
$buf = New-Object System.Text.StringBuilder
|
||||
$inQuote = $null
|
||||
for ($i = 0; $i -lt $s.Length; $i++) {
|
||||
@@ -707,16 +836,42 @@ function Parse-AvailableValueList {
|
||||
}
|
||||
}
|
||||
if ($buf.Length -gt 0) { $items += $buf.ToString() }
|
||||
return ,$items
|
||||
}
|
||||
|
||||
# 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
|
||||
function Strip-Quotes {
|
||||
# Strips a single surrounding pair of matching quotes; trims first.
|
||||
param([string]$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
|
||||
}
|
||||
|
||||
function Parse-ValueList {
|
||||
# Returns array of value strings (quotes stripped) split by top-level commas.
|
||||
# No ':' handling — values may contain colons (e.g. dateTime 2024-01-01T12:30:00).
|
||||
param([string]$s)
|
||||
$result = @()
|
||||
if ($null -eq $s) { return ,$result }
|
||||
foreach ($raw in (Split-QuotedCsv $s)) {
|
||||
$v = Strip-Quotes $raw
|
||||
if ($v -ne "") { $result += $v }
|
||||
}
|
||||
return ,$result
|
||||
}
|
||||
|
||||
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 }
|
||||
|
||||
$items = Split-QuotedCsv $s
|
||||
$stripQuotes = { param($t) Strip-Quotes $t }
|
||||
|
||||
foreach ($raw in $items) {
|
||||
$item = $raw.Trim()
|
||||
@@ -1135,7 +1290,14 @@ function Build-ParamFragment {
|
||||
}
|
||||
|
||||
$vla = [bool]$parsed.valueListAllowed
|
||||
if ($null -ne $parsed.value) {
|
||||
$valIsArray = ($parsed.value -is [array]) -or ($parsed.value -is [System.Collections.IList] -and $parsed.value -isnot [string])
|
||||
if ($valIsArray) {
|
||||
# Multi-value default (value-list): one <value> per item
|
||||
foreach ($v in $parsed.value) {
|
||||
$valueLines = Build-ParamValueXml -type $parsed.type -value $v -indent "$i`t"
|
||||
foreach ($vl in $valueLines) { $lines += $vl }
|
||||
}
|
||||
} elseif ($null -ne $parsed.value) {
|
||||
if (Test-EmptyValue $parsed.value) {
|
||||
$emptyXml = Build-EmptyValueXml -type $parsed.type -indent "$i`t" -tagPrefix "" -tagName "value" -valueListAllowed $vla
|
||||
if ($emptyXml) { $lines += $emptyXml }
|
||||
@@ -2292,6 +2454,20 @@ switch ($Operation) {
|
||||
$avPart = $rest.Substring($avIdx)
|
||||
}
|
||||
|
||||
# Separate a multi-value value=... (list) — kv-regex below grabs only a single
|
||||
# \S+ token, so a comma-separated list (with spaces) wouldn't be captured.
|
||||
# availableValue already peeled, so 'value=' here is the real value key.
|
||||
$valueListItems = $null
|
||||
$vlIdx = $simpleRest.IndexOf('value=')
|
||||
if ($vlIdx -ge 0) {
|
||||
$vlRhs = $simpleRest.Substring($vlIdx + 'value='.Length)
|
||||
$cand = Parse-ValueList $vlRhs
|
||||
if ($cand.Count -ge 2) {
|
||||
$valueListItems = $cand
|
||||
$simpleRest = $simpleRest.Substring(0, $vlIdx).Trim()
|
||||
}
|
||||
}
|
||||
|
||||
# Process simple key=value pairs (use, denyIncompleteValues, value, etc.)
|
||||
if ($simpleRest) {
|
||||
$kvPairs = [regex]::Matches($simpleRest, '(\w+)=(\S+)')
|
||||
@@ -2337,14 +2513,20 @@ switch ($Operation) {
|
||||
$fragXml = $valueLines -join "`n"
|
||||
}
|
||||
|
||||
$wasExisting = ($null -ne $existing)
|
||||
if ($existing) {
|
||||
# Capture position by next-element sibling, then remove existing
|
||||
$refNode = $existing.NextSibling
|
||||
# Collect ALL existing <value> (a param may carry a value-list) — scalar
|
||||
# value= collapses them to one, so remove every <value>, not just the first.
|
||||
$allValueEls = @()
|
||||
foreach ($ch in $paramEl.ChildNodes) {
|
||||
if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'value' -and $ch.NamespaceURI -eq $schNs) { $allValueEls += $ch }
|
||||
}
|
||||
$wasExisting = ($allValueEls.Count -gt 0)
|
||||
if ($wasExisting) {
|
||||
# Capture position after the last existing value, then remove all
|
||||
$refNode = $allValueEls[$allValueEls.Count - 1].NextSibling
|
||||
while ($refNode -and ($refNode.NodeType -eq 'Whitespace' -or $refNode.NodeType -eq 'SignificantWhitespace')) {
|
||||
$refNode = $refNode.NextSibling
|
||||
}
|
||||
Remove-NodeWithWhitespace $existing
|
||||
foreach ($ve in $allValueEls) { Remove-NodeWithWhitespace $ve }
|
||||
} else {
|
||||
# Insert before useRestriction/availableValue/denyIncompleteValues/use
|
||||
$refNode = $null
|
||||
@@ -2385,6 +2567,60 @@ switch ($Operation) {
|
||||
}
|
||||
}
|
||||
|
||||
# Process multi-value list (value=v1, v2, ...) — replace ALL <value>, ensure valueListAllowed=true
|
||||
if ($valueListItems) {
|
||||
# Declared 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
|
||||
}
|
||||
}
|
||||
}
|
||||
# Remove ALL existing <value>; capture insertion ref after the last one
|
||||
$valueEls = @()
|
||||
foreach ($child in $paramEl.ChildNodes) {
|
||||
if ($child.NodeType -eq 'Element' -and $child.LocalName -eq 'value' -and $child.NamespaceURI -eq $schNs) { $valueEls += $child }
|
||||
}
|
||||
$refNode = $null
|
||||
if ($valueEls.Count -gt 0) {
|
||||
$refNode = $valueEls[$valueEls.Count - 1].NextSibling
|
||||
while ($refNode -and ($refNode.NodeType -eq 'Whitespace' -or $refNode.NodeType -eq 'SignificantWhitespace')) { $refNode = $refNode.NextSibling }
|
||||
foreach ($ve in $valueEls) { Remove-NodeWithWhitespace $ve }
|
||||
} else {
|
||||
foreach ($child in $paramEl.ChildNodes) {
|
||||
if ($child.NodeType -eq 'Element' -and $child.LocalName -in @('useRestriction','availableValue','denyIncompleteValues','use')) { $refNode = $child; break }
|
||||
}
|
||||
}
|
||||
foreach ($v in $valueListItems) {
|
||||
$fragXml = (Build-ParamValueXml -type $declaredType -value $v -indent $childIndent) -join "`n"
|
||||
$nodes = Import-Fragment $xmlDoc $fragXml
|
||||
foreach ($node in $nodes) { Insert-BeforeElement $paramEl $node $refNode $childIndent }
|
||||
}
|
||||
# Ensure <valueListAllowed>true</valueListAllowed> (schema order: after useRestriction, before availableValue/use)
|
||||
$vlaEl = $null
|
||||
foreach ($ch in $paramEl.ChildNodes) {
|
||||
if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'valueListAllowed' -and $ch.NamespaceURI -eq $schNs) { $vlaEl = $ch; break }
|
||||
}
|
||||
if ($vlaEl) {
|
||||
if ($vlaEl.InnerText.Trim() -ne 'true') { $vlaEl.InnerText = 'true' }
|
||||
} else {
|
||||
$refVla = $null
|
||||
foreach ($child in $paramEl.ChildNodes) {
|
||||
if ($child.NodeType -eq 'Element' -and $child.LocalName -in @('availableValue','denyIncompleteValues','use')) { $refVla = $child; break }
|
||||
}
|
||||
$nodes = Import-Fragment $xmlDoc "$childIndent<valueListAllowed>true</valueListAllowed>"
|
||||
foreach ($node in $nodes) { Insert-BeforeElement $paramEl $node $refVla $childIndent }
|
||||
}
|
||||
$script:Dirty = $true; Write-Host "[OK] Parameter `"$paramName`": value set to list of $($valueListItems.Count) item(s)"
|
||||
}
|
||||
|
||||
# Process availableValue — replace whole list with new items
|
||||
if ($avPart) {
|
||||
$avRest = ($avPart -replace '^availableValue=', '').Trim()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# skd-edit v1.24 — Atomic 1C DCS editor (Python port)
|
||||
# skd-edit v1.27 — Atomic 1C DCS editor (Python port)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
@@ -118,6 +119,169 @@ if not os.path.exists(template_path):
|
||||
sys.exit(1)
|
||||
|
||||
resolved_path = os.path.abspath(template_path)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md
|
||||
# Blocks edits of vendor objects "на замке" / read-only configs. Trigger = bin
|
||||
# present; reaction from .v8-project.json editingAllowedCheck (deny|warn|off,
|
||||
# default deny). Never throws (except sys.exit on deny) — errors degrade to allow.
|
||||
# ============================================================
|
||||
|
||||
def _sg_root_uuid(xml_path):
|
||||
if not os.path.isfile(xml_path):
|
||||
return None
|
||||
try:
|
||||
mx = etree.parse(xml_path).getroot()
|
||||
for child in mx:
|
||||
if isinstance(child.tag, str) and child.get("uuid"):
|
||||
return child.get("uuid")
|
||||
except Exception:
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def _sg_find_v8project(start_dir):
|
||||
d = start_dir
|
||||
for _ in range(20):
|
||||
if not d:
|
||||
break
|
||||
pj = os.path.join(d, ".v8-project.json")
|
||||
if os.path.isfile(pj):
|
||||
return pj
|
||||
parent = os.path.dirname(d)
|
||||
if parent == d:
|
||||
break
|
||||
d = parent
|
||||
return None
|
||||
|
||||
|
||||
def _sg_get_edit_mode(cfg_dir):
|
||||
try:
|
||||
pj = _sg_find_v8project(os.getcwd()) or _sg_find_v8project(cfg_dir)
|
||||
if not pj:
|
||||
return "deny"
|
||||
proj = json.loads(open(pj, encoding="utf-8-sig").read())
|
||||
cfg_full = os.path.normcase(os.path.abspath(cfg_dir)).rstrip("\\/")
|
||||
for db in proj.get("databases", []):
|
||||
src = db.get("configSrc")
|
||||
if src:
|
||||
src_full = os.path.normcase(os.path.abspath(src)).rstrip("\\/")
|
||||
if cfg_full == src_full or cfg_full.startswith(src_full + os.sep):
|
||||
if db.get("editingAllowedCheck"):
|
||||
return db["editingAllowedCheck"]
|
||||
if proj.get("editingAllowedCheck"):
|
||||
return proj["editingAllowedCheck"]
|
||||
return "deny"
|
||||
except Exception:
|
||||
return "deny"
|
||||
|
||||
|
||||
def assert_edit_allowed(target_path, require):
|
||||
try:
|
||||
rp = os.path.abspath(target_path)
|
||||
elem_uuid = _sg_root_uuid(rp)
|
||||
cfg_dir = None
|
||||
bin_path = None
|
||||
d = rp if os.path.isdir(rp) else os.path.dirname(rp)
|
||||
for _ in range(12):
|
||||
if not d:
|
||||
break
|
||||
if not elem_uuid:
|
||||
elem_uuid = _sg_root_uuid(d + ".xml")
|
||||
if not cfg_dir:
|
||||
cand = os.path.join(d, "Ext", "ParentConfigurations.bin")
|
||||
if os.path.exists(cand) or os.path.exists(os.path.join(d, "Configuration.xml")):
|
||||
cfg_dir = d
|
||||
bin_path = cand
|
||||
if elem_uuid and cfg_dir:
|
||||
break
|
||||
parent = os.path.dirname(d)
|
||||
if parent == d:
|
||||
break
|
||||
d = parent
|
||||
if not elem_uuid and cfg_dir:
|
||||
elem_uuid = _sg_root_uuid(os.path.join(cfg_dir, "Configuration.xml"))
|
||||
if not bin_path or not os.path.exists(bin_path):
|
||||
return
|
||||
data = open(bin_path, "rb").read()
|
||||
if len(data) <= 32:
|
||||
return
|
||||
if data[:3] == b"\xef\xbb\xbf":
|
||||
data = data[3:]
|
||||
text = data.decode("utf-8", "replace")
|
||||
h = re.match(r"\{6,(\d+),(\d+),", text)
|
||||
if not h:
|
||||
return
|
||||
g = int(h.group(1))
|
||||
k = int(h.group(2))
|
||||
if k == 0:
|
||||
return
|
||||
best = None
|
||||
if elem_uuid:
|
||||
for m in re.finditer(r"([0-2]),0," + re.escape(elem_uuid.lower()), text):
|
||||
f1 = int(m.group(1))
|
||||
if best is None or f1 < best:
|
||||
best = f1
|
||||
blocked = False
|
||||
code = ""
|
||||
reason = ""
|
||||
if g == 1:
|
||||
blocked = True
|
||||
code = "capability-off"
|
||||
reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)"
|
||||
elif require == "removed":
|
||||
if best is not None and best != 2:
|
||||
blocked = True
|
||||
code = "not-removed"
|
||||
reason = "объект не снят с поддержки — удаление сломает обновления"
|
||||
else:
|
||||
if best is not None and best == 0:
|
||||
blocked = True
|
||||
code = "locked"
|
||||
reason = "объект на замке — редактирование сломает обновления"
|
||||
if not blocked:
|
||||
return
|
||||
mode = _sg_get_edit_mode(cfg_dir)
|
||||
if mode == "off":
|
||||
return
|
||||
if mode == "warn":
|
||||
sys.stderr.write(f"[support-guard] ПРЕДУПРЕЖДЕНИЕ: {reason}. Цель: {rp}\n")
|
||||
return
|
||||
head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления."
|
||||
cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются."
|
||||
off_note = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json."
|
||||
if code == "capability-off":
|
||||
state = f"Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «{rp}» редактировать нельзя."
|
||||
fix = (
|
||||
"Либо снять защиту явно (навык support-edit, два шага):\n"
|
||||
f' 1. support-edit -Path "{cfg_dir}" -Capability on — включить возможность изменения (объекты пока остаются на замке);\n'
|
||||
f' 2. support-edit -Path "{rp}" -Set editable — открыть этот объект для редактирования.\n'
|
||||
" Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора."
|
||||
)
|
||||
elif code == "not-removed":
|
||||
state = f"Состояние: объект «{rp}» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора."
|
||||
fix = (
|
||||
"Либо сначала снять объект с поддержки, затем удалять:\n"
|
||||
f' support-edit -Path "{rp}" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно.'
|
||||
)
|
||||
else:
|
||||
state = f"Состояние: объект «{rp}» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)."
|
||||
fix = (
|
||||
"Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):\n"
|
||||
f' support-edit -Path "{rp}" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);\n'
|
||||
f' support-edit -Path "{rp}" -Set off-support — снять с поддержки: обновления по объекту больше не приходят.'
|
||||
)
|
||||
sys.stderr.write(head + "\n" + state + "\n" + cfe + "\n" + fix + "\n" + off_note + "\n")
|
||||
sys.exit(1)
|
||||
except SystemExit:
|
||||
raise
|
||||
except Exception:
|
||||
return
|
||||
|
||||
|
||||
assert_edit_allowed(resolved_path, "editable")
|
||||
|
||||
query_base_dir = os.path.dirname(resolved_path)
|
||||
|
||||
# ── 2. Type system ──────────────────────────────────────────
|
||||
@@ -371,7 +535,18 @@ def parse_param_shorthand(s):
|
||||
result["name"] = m.group(1).strip()
|
||||
result["type"] = resolve_type_str(m.group(2).strip())
|
||||
if m.group(3) is not None:
|
||||
result["value"] = m.group(4).strip() if m.group(4) else ""
|
||||
rhs = m.group(4)
|
||||
if rhs and rhs.strip():
|
||||
items = parse_value_list(rhs.strip())
|
||||
if len(items) >= 2:
|
||||
# Multi-value default → list; valueListAllowed implied
|
||||
result["value"] = items
|
||||
result["valueListAllowed"] = True
|
||||
else:
|
||||
# Scalar (single item, quotes stripped) or empty sentinel
|
||||
result["value"] = items[0] if len(items) == 1 else ""
|
||||
else:
|
||||
result["value"] = ""
|
||||
else:
|
||||
result["name"] = s.strip()
|
||||
|
||||
@@ -631,14 +806,12 @@ 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
|
||||
def split_quoted_csv(s):
|
||||
"""Split on top-level commas, respecting single/double quoted spans.
|
||||
Returns raw (un-stripped) item spans. Shared by availableValue and value-list parsing."""
|
||||
items = []
|
||||
if s is None:
|
||||
return items
|
||||
buf = []
|
||||
in_quote = None
|
||||
for ch in s:
|
||||
@@ -656,12 +829,37 @@ def parse_available_value_list(s):
|
||||
buf.append(ch)
|
||||
if buf:
|
||||
items.append("".join(buf))
|
||||
return items
|
||||
|
||||
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
|
||||
|
||||
def strip_quotes(t):
|
||||
"""Strip a single surrounding pair of matching quotes; trims first."""
|
||||
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
|
||||
|
||||
|
||||
def parse_value_list(s):
|
||||
"""Return list of value strings (quotes stripped) split by top-level commas.
|
||||
No ':' handling — values may contain colons (e.g. dateTime 2024-01-01T12:30:00)."""
|
||||
if s is None:
|
||||
return []
|
||||
result = []
|
||||
for raw in split_quoted_csv(s):
|
||||
v = strip_quotes(raw)
|
||||
if v != "":
|
||||
result.append(v)
|
||||
return result
|
||||
|
||||
|
||||
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 []
|
||||
|
||||
items = split_quoted_csv(s)
|
||||
|
||||
result = []
|
||||
for raw in items:
|
||||
@@ -1012,7 +1210,12 @@ def build_param_fragment(parsed, indent):
|
||||
lines.append(f"{i}\t</valueType>")
|
||||
|
||||
vla = bool(parsed.get("valueListAllowed"))
|
||||
if parsed["value"] is not None:
|
||||
if isinstance(parsed["value"], list):
|
||||
# Multi-value default (value-list): one <value> per item
|
||||
for v in parsed["value"]:
|
||||
for vl in build_param_value_xml(parsed.get("type", ""), v, f"{i}\t"):
|
||||
lines.append(vl)
|
||||
elif parsed["value"] is not None:
|
||||
if is_empty_value(parsed["value"]):
|
||||
empty_xml = build_empty_value_xml(parsed.get("type", ""), f"{i}\t", "", "value", vla)
|
||||
if empty_xml:
|
||||
@@ -1987,6 +2190,17 @@ elif operation == "modify-parameter":
|
||||
simple_rest = rest[:av_idx].strip()
|
||||
av_part = rest[av_idx:]
|
||||
|
||||
# Separate a multi-value value=... (list) — kv-regex below grabs only a single
|
||||
# \S+ token, so a comma-separated list (with spaces) wouldn't be captured.
|
||||
value_list_items = None
|
||||
vl_idx = simple_rest.find("value=")
|
||||
if vl_idx >= 0:
|
||||
vl_rhs = simple_rest[vl_idx + len("value="):]
|
||||
cand = parse_value_list(vl_rhs)
|
||||
if len(cand) >= 2:
|
||||
value_list_items = cand
|
||||
simple_rest = simple_rest[:vl_idx].strip()
|
||||
|
||||
# Process simple key=value pairs (use, denyIncompleteValues, etc.)
|
||||
if simple_rest:
|
||||
for m in re.finditer(r'(\w+)=(\S+)', simple_rest):
|
||||
@@ -2012,12 +2226,15 @@ elif operation == "modify-parameter":
|
||||
else:
|
||||
value_lines = build_param_value_xml(declared_type, value, child_indent)
|
||||
frag_xml = "\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)
|
||||
# Collect ALL existing <value> (a param may carry a value-list) — scalar
|
||||
# value= collapses them to one, so remove every <value>, not just the first.
|
||||
all_value_els = [ch for ch in param_el if isinstance(ch.tag, str) and local_name(ch) == "value" and etree.QName(ch.tag).namespace == SCH_NS]
|
||||
was_existing = len(all_value_els) > 0
|
||||
if was_existing:
|
||||
last_idx = list(param_el).index(all_value_els[-1])
|
||||
ref_node = param_el[last_idx + 1] if last_idx + 1 < len(param_el) else None
|
||||
for ve in all_value_els:
|
||||
remove_node_with_whitespace(ve)
|
||||
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)
|
||||
if frag_xml:
|
||||
@@ -2040,6 +2257,39 @@ elif operation == "modify-parameter":
|
||||
insert_before_element(param_el, node, ref_node, child_indent)
|
||||
dirty = True; print(f'[OK] Parameter "{param_name}": {key}={value} added')
|
||||
|
||||
# Process multi-value list (value=v1, v2, ...) — replace ALL <value>, ensure valueListAllowed=true
|
||||
if value_list_items:
|
||||
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
|
||||
# Remove ALL existing <value>; capture insertion ref after the last one
|
||||
value_els = [ch for ch in param_el if isinstance(ch.tag, str) and local_name(ch) == "value" and etree.QName(ch.tag).namespace == SCH_NS]
|
||||
if value_els:
|
||||
last_idx = list(param_el).index(value_els[-1])
|
||||
ref_node = param_el[last_idx + 1] if last_idx + 1 < len(param_el) else None
|
||||
for ve in value_els:
|
||||
remove_node_with_whitespace(ve)
|
||||
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)
|
||||
for v in value_list_items:
|
||||
frag_xml = "\n".join(build_param_value_xml(declared_type, v, child_indent))
|
||||
for node in import_fragment(xml_doc, frag_xml):
|
||||
insert_before_element(param_el, node, ref_node, child_indent)
|
||||
# Ensure <valueListAllowed>true</valueListAllowed> (schema order: after useRestriction, before availableValue/use)
|
||||
vla_el = next((ch for ch in param_el if isinstance(ch.tag, str) and local_name(ch) == "valueListAllowed" and etree.QName(ch.tag).namespace == SCH_NS), None)
|
||||
if vla_el is not None:
|
||||
if (vla_el.text or "").strip() != "true":
|
||||
vla_el.text = "true"
|
||||
else:
|
||||
ref_vla = 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}<valueListAllowed>true</valueListAllowed>"):
|
||||
insert_before_element(param_el, node, ref_vla, child_indent)
|
||||
dirty = True; print(f'[OK] Parameter "{param_name}": value set to list of {len(value_list_items)} item(s)')
|
||||
|
||||
# Process availableValue
|
||||
if av_part:
|
||||
av_rest = av_part[len("availableValue="):].strip()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: skd-info
|
||||
description: Анализ структуры схемы компоновки данных 1С (СКД) — наборы, поля, параметры, варианты. Используй для понимания отчёта — источник данных (запрос), доступные поля, параметры
|
||||
argument-hint: <TemplatePath> [-Mode overview|query|fields|links|calculated|resources|params|variant|templates|trace|full] [-Name <dataset|variant|field|group>]
|
||||
argument-hint: <TemplatePath> [-Mode overview|query|fields|links|calculated|resources|params|variant|templates|trace|full] [-Name <dataset|variant|field|group>] [-Raw]
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
@@ -20,7 +20,8 @@ allowed-tools:
|
||||
| `Mode` | Режим анализа (по умолчанию `overview`) |
|
||||
| `Name` | Имя набора (query), поля (fields/calculated/resources/trace), варианта (variant) или группировки/поля (templates) |
|
||||
| `Batch` | Номер пакета запроса, 0 = все (только query) |
|
||||
| `Limit` / `Offset` | Пагинация (по умолчанию 150 строк) |
|
||||
| `Raw` | (только query) сырой текст запроса целиком, без заголовков/оглавления/разделителей пакетов. Для выгрузки в `.sql` и возврата через `skd-edit set-query @file` |
|
||||
| `Limit` / `Offset` | Пагинация (по умолчанию 150 строк; `-Raw` не усекается) |
|
||||
| `OutFile` | Записать результат в файл (UTF-8 BOM) |
|
||||
|
||||
```powershell
|
||||
@@ -31,6 +32,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/skd-info.ps1" -Temp
|
||||
```powershell
|
||||
... -Mode query -Name НоменклатураСЦенами
|
||||
... -Mode query -Name ДанныеТ13 -Batch 3
|
||||
... -Mode query -Name ДанныеТ13 -Raw -OutFile query.sql
|
||||
... -Mode fields -Name КадастроваяСтоимость
|
||||
... -Mode calculated -Name КоэффициентКи
|
||||
... -Mode resources -Name СуммаНалога
|
||||
@@ -45,7 +47,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/skd-info.ps1" -Temp
|
||||
| Режим | Без `-Name` | С `-Name` |
|
||||
|-------|-------------|-----------|
|
||||
| `overview` | Навигационная карта схемы + подсказки Next | — |
|
||||
| `query` | — | Текст запроса набора (с оглавлением батчей) |
|
||||
| `query` | — | Текст запроса набора (с оглавлением батчей); `-Raw` — чистая выгрузка для правки |
|
||||
| `fields` | Карта: имена полей по наборам | Деталь поля: набор, тип, роль, формат |
|
||||
| `links` | Все связи наборов | — |
|
||||
| `calculated` | Карта: имена вычисляемых полей | Выражение + заголовок + ограничения |
|
||||
@@ -65,7 +67,10 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/skd-info.ps1" -Temp
|
||||
3. `query -Name <набор>` — посмотреть текст SQL-запроса
|
||||
4. `variant -Name <N>` — посмотреть группировки и фильтры варианта
|
||||
|
||||
Подробные примеры вывода каждого режима — в `modes-reference.md`.
|
||||
Переработка запроса (round-trip): `query -Name <набор> -Raw -OutFile q.sql` →
|
||||
правка `q.sql` → `/skd-edit <tpl> -Operation set-query -Value "@q.sql"`. Флаг
|
||||
`-Raw` отдаёт запрос целиком без декораций, поэтому выгрузка ↔ возврат
|
||||
точны (включая многопакетные запросы с временными таблицами).
|
||||
|
||||
## Верификация
|
||||
|
||||
|
||||
@@ -1,246 +0,0 @@
|
||||
# /skd-info — полная справка по режимам
|
||||
|
||||
Компактное описание — в [SKILL.md](SKILL.md).
|
||||
|
||||
## overview (по умолчанию) — карта схемы
|
||||
|
||||
Компактная навигационная карта (10-25 строк). Показывает структуру и подсказывает следующие шаги:
|
||||
|
||||
```
|
||||
=== DCS: ОсновнаяСхемаКомпоновкиДанных (362 lines) ===
|
||||
|
||||
Sources: ИсточникДанных1 (Local)
|
||||
|
||||
Datasets:
|
||||
[Query] НоменклатураСЦенами 7 fields, query 40 lines
|
||||
Calculated: 1
|
||||
Resources: 1
|
||||
Templates: 1 templates, 1 group bindings
|
||||
Params: (none)
|
||||
|
||||
Variants:
|
||||
[1] НоменклатураИЦены "Номенклатура и цены" Table(detail) 3 filters
|
||||
[2] НоменклатураБезЦен "Номенклатура без цен" Group(detail) 2 filters
|
||||
|
||||
Next:
|
||||
-Mode query query text
|
||||
-Mode fields field tables by dataset
|
||||
-Mode calculated calculated field expressions
|
||||
-Mode resources resource aggregation
|
||||
-Mode variant -Name <N> variant structure (1..2)
|
||||
```
|
||||
|
||||
Для DataSetUnion — дерево наборов + связи:
|
||||
```
|
||||
Datasets:
|
||||
[Union] РасчетНалогаНаИмущество 52 fields
|
||||
├─ [Query] РасчетНалогаНаИмущество 51 fields, query 181 lines
|
||||
├─ [Query] ДанныеПоКадастровой 29 fields, query 40 lines
|
||||
├─ [Query] ДанныеПоСреднегодовой 34 fields, query 41 lines
|
||||
Links: РасчетНалогаНаИмущество -> СостояниеОС (2 fields)
|
||||
```
|
||||
|
||||
Параметры разделяются на видимые/скрытые:
|
||||
```
|
||||
Params: 18 (7 visible, 11 hidden): Период, Ответственный, ...
|
||||
```
|
||||
|
||||
## query — текст запроса
|
||||
|
||||
`-Name <набор>` — имя DataSet (обязателен если наборов > 1).
|
||||
|
||||
Извлекает raw-текст запроса с деэкранированием XML (`&`→`&`, `>`→`>`). Для пакетных запросов — оглавление батчей:
|
||||
|
||||
```
|
||||
=== Query: ДанныеТ13 (334 lines, 13 batches) ===
|
||||
Batch 1: lines 1-8 → ПОМЕСТИТЬ Представления_Периоды
|
||||
Batch 2: lines 9-26 → ПОМЕСТИТЬ Представления_СотрудникиОрганизации
|
||||
...
|
||||
--- Batch 1 ---
|
||||
ВЫБРАТЬ
|
||||
ДАТАВРЕМЯ(1, 1, 1) КАК Период
|
||||
ПОМЕСТИТЬ Представления_Периоды
|
||||
...
|
||||
```
|
||||
|
||||
Фильтр по номеру батча: `-Batch 3` покажет только 3-й пакет.
|
||||
|
||||
## fields — поля наборов данных
|
||||
|
||||
Без `-Name` — карта: имена полей по наборам:
|
||||
```
|
||||
=== Fields map ===
|
||||
СостояниеОС [Query] (3): Организация, ОсновноеСредство, ДатаСостояния
|
||||
РасчетНалогаНаИмущество [Union] (52): ДоляСтоимостиЧислитель, ...
|
||||
РасчетНалогаНаИмущество [Query] (51): КадастроваяСтоимость, ...
|
||||
```
|
||||
|
||||
С `-Name <поле>` — детали конкретного поля:
|
||||
```
|
||||
=== Field: ДатаСостояния "Дата ввода в эксплуатацию" ===
|
||||
|
||||
Dataset: СостояниеОС [Query]
|
||||
Format: ДФ=dd.MM.yyyy
|
||||
```
|
||||
|
||||
Показывает: dataset, title, type, role, useRestriction, format, presentationExpression.
|
||||
|
||||
## links — связи наборов данных
|
||||
|
||||
```
|
||||
=== Links (4) ===
|
||||
|
||||
РасчетНалогаНаИмущество -> СостояниеОС :
|
||||
Организация -> Организация
|
||||
ОсновноеСредство -> ОсновноеСредство
|
||||
```
|
||||
|
||||
Группирует по парам наборов. Показывает поля связи и параметры.
|
||||
|
||||
## calculated — вычисляемые поля
|
||||
|
||||
Без `-Name` — карта: имена и заголовки:
|
||||
```
|
||||
=== Calculated fields (23) ===
|
||||
ДоляСтоимости "Доля стоимости"
|
||||
КоэффициентКи "Коэффициент Ки"
|
||||
...
|
||||
```
|
||||
|
||||
С `-Name <поле>` — полное выражение:
|
||||
```
|
||||
=== Calculated: ДоляСтоимости ===
|
||||
|
||||
Expression:
|
||||
ВЫБОР КОГДА ... ТОГДА "1" ИНАЧЕ ... КОНЕЦ
|
||||
Title: Доля стоимости
|
||||
Restrict: condition
|
||||
```
|
||||
|
||||
## resources — ресурсы (итоги по группировкам)
|
||||
|
||||
Без `-Name` — карта: имена полей, `*` = есть формулы по группировкам:
|
||||
```
|
||||
=== Resources (51) ===
|
||||
НалоговаяБаза
|
||||
КоэффициентКи *
|
||||
...
|
||||
* = has group-level formulas
|
||||
```
|
||||
|
||||
С `-Name <поле>` — формулы агрегации:
|
||||
```
|
||||
=== Resource: ДатаСостояния ===
|
||||
|
||||
[ОсновноеСредство] ЕстьNull(ДатаСостояния, "")
|
||||
```
|
||||
|
||||
## params — параметры схемы
|
||||
|
||||
```
|
||||
=== Parameters (16) ===
|
||||
Name Type Default Visible Expression
|
||||
Период StandardPeriod LastMonth yes -
|
||||
НачалоПериода DateTime - hidden &Период.ДатаНачала
|
||||
Организация CatalogRef.Организации null yes -
|
||||
```
|
||||
|
||||
## variant — варианты отчёта
|
||||
|
||||
Без `-Name` — список вариантов:
|
||||
```
|
||||
=== Variants (2) ===
|
||||
[1] НоменклатураИЦены "Номенклатура и цены" Table(detail) 3 filters
|
||||
[2] НоменклатураБезЦен "Номенклатура без цен" Group(detail) 2 filters
|
||||
```
|
||||
|
||||
С `-Name <N|имя>` — структура конкретного варианта:
|
||||
```
|
||||
=== Variant [1]: НоменклатураИЦены "Номенклатура и цены" ===
|
||||
|
||||
Structure:
|
||||
Table "Таблица"
|
||||
├── Columns: [ТипЦен Items]
|
||||
│ Selection: Auto, Цена
|
||||
└── Rows: [Номенклатура Items]
|
||||
Selection: Номенклатура, УИД, Auto
|
||||
|
||||
Filter:
|
||||
[ ] Номенклатура InHierarchy [user]
|
||||
[ ] ТипЦен Equal
|
||||
[x] ВАрхиве = false "Исключая скрытые товары"
|
||||
|
||||
DataParams: КлючВарианта="НоменклатураИЦены"
|
||||
Output: style=ЧерноБелый groups=Separately totalsH=None totalsV=None
|
||||
```
|
||||
|
||||
## templates — привязки шаблонов вывода
|
||||
|
||||
Три типа привязок: `fieldTemplate` (к полю), `groupTemplate` (к группировке, Header/Footer), `groupHeaderTemplate` (заголовок группы).
|
||||
|
||||
Без `-Name` — карта привязок:
|
||||
```
|
||||
=== Templates (70 defined: 49 field, 37 group) ===
|
||||
|
||||
Field bindings (49): (all trivial)
|
||||
ОстаточнаяСтоимостьНа0101, ОстаточнаяСтоимостьНа0102, ...
|
||||
|
||||
Group bindings (37):
|
||||
ВидНалоговойБазы
|
||||
Header -> Макет3 (1 rows, 1 params)
|
||||
СреднегодоваяСтоимость2019
|
||||
Footer -> Макет50 (1 rows) spacer
|
||||
GroupHeader -> Макет40 (3 rows)
|
||||
```
|
||||
|
||||
С `-Name <группировка|поле>` — содержимое шаблонов:
|
||||
```
|
||||
=== Templates: СреднегодоваяСтоимость2019 ===
|
||||
|
||||
Footer -> Макет50 [1 rows, 1 cells]:
|
||||
Row 1: (empty)
|
||||
|
||||
GroupHeader -> Макет40 [3 rows, 78 cells]:
|
||||
Row 1: "№ п/п" | "###Группировки1###" | "Инв. номер" | ...
|
||||
Row 2: "01.01" | "01.02" | ... | "31.12"
|
||||
Row 3: "1" | "2" | ... | "26"
|
||||
```
|
||||
|
||||
Для field-привязок:
|
||||
```
|
||||
=== Field template: ОстаточнаяСтоимостьНа0101 -> Макет4 ===
|
||||
[1 rows, 1 cells]
|
||||
Row 1: {ОстаточнаяСтоимостьНа0101}
|
||||
(all params trivial)
|
||||
```
|
||||
|
||||
**Тривиальность выражений**: `Поле = Поле` и `Поле = Представление(Поле)` считаются тривиальными и НЕ выводятся. Показываются только нетривиальные — когда выражение содержит другое поле, вызов метода, пустую строку и т.д.
|
||||
|
||||
## trace — трассировка поля от заголовка до запроса
|
||||
|
||||
Ищет поле по dataPath ИЛИ заголовку (включая подстроку) и показывает полную цепочку происхождения за один вызов:
|
||||
|
||||
```
|
||||
=== Trace: КоэффициентКи "Коэффициент Ки" ===
|
||||
|
||||
Dataset: (schema-level only, not in dataset fields)
|
||||
|
||||
Calculated:
|
||||
ВЫБОР КОГДА ... ТОГДА 0 ИНАЧЕ ... КОНЕЦ
|
||||
Operands:
|
||||
КоличествоМесяцевИспользования -> РасчетНалогаНаИмущество [Query]
|
||||
КоличествоМесяцевВладения -> РасчетНалогаНаИмущество [Query]
|
||||
|
||||
Resource:
|
||||
[ОсновноеСредство] Сумма(КоэффициентКи)
|
||||
```
|
||||
|
||||
Типичный сценарий: пользователь видит колонку "Коэффициент Ки" в отчёте и спрашивает как она считается. Один вызов `trace` показывает: формулу вычисления, откуда берутся операнды, как агрегируется в ресурс.
|
||||
|
||||
## Что не выводится
|
||||
|
||||
- XML namespace-декларации
|
||||
- Обёртки v8:item/v8:lang/v8:content (извлекаем чистый текст)
|
||||
- userSettingID (GUID-ы пользовательских настроек)
|
||||
- Дефолтные periodAdditionBegin/End = 0001-01-01
|
||||
- viewMode
|
||||
@@ -1,4 +1,4 @@
|
||||
# skd-info v1.5 — Analyze 1C DCS structure
|
||||
# skd-info v1.7 — Analyze 1C DCS structure
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory=$true)]
|
||||
@@ -10,7 +10,8 @@ param(
|
||||
[int]$Batch = 0,
|
||||
[int]$Limit = 150,
|
||||
[int]$Offset = 0,
|
||||
[string]$OutFile
|
||||
[string]$OutFile,
|
||||
[switch]$Raw
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
@@ -333,8 +334,68 @@ for ($i = $pathParts.Count - 1; $i -ge 0; $i--) {
|
||||
|
||||
$totalXmlLines = (Get-Content $resolvedPath).Count
|
||||
|
||||
function Get-SupportStatusForPath([string]$targetPath) {
|
||||
try {
|
||||
$rp = (Resolve-Path $targetPath).Path
|
||||
$elemUuid = $null
|
||||
$binPath = $null
|
||||
# Reads the uuid of the first metadata element in an .xml file (or $null).
|
||||
function Get-RootUuid([string]$xmlPath) {
|
||||
if (-not (Test-Path $xmlPath)) { return $null }
|
||||
try {
|
||||
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
|
||||
$el = $mx.DocumentElement.FirstChild
|
||||
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
|
||||
if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } }
|
||||
} catch {}
|
||||
return $null
|
||||
}
|
||||
# The target file itself may be the element meta-xml (e.g. Subsystems/X.xml).
|
||||
$elemUuid = Get-RootUuid $rp
|
||||
$d = [System.IO.Path]::GetDirectoryName($rp)
|
||||
for ($i = 0; $i -lt 12 -and $d; $i++) {
|
||||
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
|
||||
if (-not $binPath) {
|
||||
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
|
||||
if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $binPath = $cand }
|
||||
}
|
||||
if ($elemUuid -and $binPath) { break }
|
||||
$parent = [System.IO.Path]::GetDirectoryName($d)
|
||||
if ($parent -eq $d) { break }
|
||||
$d = $parent
|
||||
}
|
||||
if (-not $binPath -or -not (Test-Path $binPath)) { return "не на поддержке" }
|
||||
$bytes = [System.IO.File]::ReadAllBytes($binPath)
|
||||
if ($bytes.Length -le 32) { return "снято с поддержки (правки свободны)" }
|
||||
$start = 0
|
||||
if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 }
|
||||
$text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start)
|
||||
$h = [regex]::Match($text, '^\{6,(\d+),(\d+),')
|
||||
if (-not $h.Success) { return "не на поддержке" }
|
||||
$G = [int]$h.Groups[1].Value
|
||||
$K = [int]$h.Groups[2].Value
|
||||
if ($K -eq 0) { return "снято с поддержки (правки свободны)" }
|
||||
if ($G -eq 1) { return "конфигурация read-only (возможность изменения выключена) — правки невозможны без включения" }
|
||||
if (-not $elemUuid) { return "не на поддержке" }
|
||||
$u = [regex]::Escape($elemUuid.ToLower())
|
||||
$best = $null
|
||||
foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) {
|
||||
$f1 = [int]$m.Groups[1].Value
|
||||
if ($null -eq $best -or $f1 -lt $best) { $best = $f1 }
|
||||
}
|
||||
if ($null -eq $best) { return "не на поддержке" }
|
||||
switch ($best) {
|
||||
0 { return "на замке — прямая правка сломает обновления; дорабатывай через cfe-* либо включи редактирование объекта" }
|
||||
1 { return "редактируется с сохранением поддержки" }
|
||||
2 { return "снято с поддержки (правки свободны)" }
|
||||
}
|
||||
return "не на поддержке"
|
||||
} catch { return "не на поддержке" }
|
||||
}
|
||||
|
||||
function Show-Overview {
|
||||
$lines.Add("=== DCS: $templateName ($totalXmlLines lines) ===")
|
||||
$lines.Add("Поддержка: $(Get-SupportStatusForPath $TemplatePath)")
|
||||
$lines.Add("")
|
||||
|
||||
# Sources
|
||||
@@ -655,6 +716,13 @@ function Show-Query {
|
||||
}
|
||||
|
||||
$rawQuery = Unescape-Xml $queryNode.InnerText
|
||||
|
||||
# Raw mode: emit verbatim query text only (no headers/TOC/batch split) for round-trip
|
||||
if ($Raw) {
|
||||
foreach ($ql in ($rawQuery.Trim() -split "`n")) { $lines.Add($ql.TrimEnd()) }
|
||||
return
|
||||
}
|
||||
|
||||
$dsNameStr = $targetDs.SelectSingleNode("s:name", $ns).InnerText
|
||||
|
||||
# Split into batches
|
||||
@@ -1894,7 +1962,7 @@ if ($Offset -gt 0) {
|
||||
$result = $result[$Offset..($totalLines - 1)]
|
||||
}
|
||||
|
||||
if ($result.Count -gt $Limit) {
|
||||
if (-not $Raw -and $result.Count -gt $Limit) {
|
||||
$shown = $result[0..($Limit - 1)]
|
||||
foreach ($l in $shown) { Write-Host $l }
|
||||
Write-Host ""
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# skd-info v1.5 — Analyze 1C DCS structure
|
||||
# skd-info v1.7 — Analyze 1C DCS structure
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
@@ -265,6 +265,74 @@ def build_structure_tree(item_node, prefix, is_last, out_lines):
|
||||
build_structure_tree(child, f"{prefix}{continuation}", last, out_lines)
|
||||
|
||||
|
||||
def get_support_status_for_path(target_path):
|
||||
try:
|
||||
def root_uuid(xml_path):
|
||||
if not os.path.isfile(xml_path):
|
||||
return None
|
||||
try:
|
||||
mx = etree.parse(xml_path).getroot()
|
||||
for child in mx:
|
||||
if isinstance(child.tag, str) and child.get("uuid"):
|
||||
return child.get("uuid")
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
rp = os.path.abspath(target_path)
|
||||
# The target file itself may be the element meta-xml (e.g. Subsystems/X.xml).
|
||||
elem_uuid = root_uuid(rp)
|
||||
bin_path = None
|
||||
d = os.path.dirname(rp)
|
||||
for _ in range(12):
|
||||
if not d:
|
||||
break
|
||||
if not elem_uuid:
|
||||
elem_uuid = root_uuid(d + ".xml")
|
||||
if not bin_path:
|
||||
cand = os.path.join(d, "Ext", "ParentConfigurations.bin")
|
||||
if os.path.exists(cand) or os.path.exists(os.path.join(d, "Configuration.xml")):
|
||||
bin_path = cand
|
||||
if elem_uuid and bin_path:
|
||||
break
|
||||
parent = os.path.dirname(d)
|
||||
if parent == d:
|
||||
break
|
||||
d = parent
|
||||
if not bin_path or not os.path.exists(bin_path):
|
||||
return "не на поддержке"
|
||||
data = open(bin_path, "rb").read()
|
||||
if len(data) <= 32:
|
||||
return "снято с поддержки (правки свободны)"
|
||||
if data[:3] == b"\xef\xbb\xbf":
|
||||
data = data[3:]
|
||||
text = data.decode("utf-8", "replace")
|
||||
h = re.match(r"\{6,(\d+),(\d+),", text)
|
||||
if not h:
|
||||
return "не на поддержке"
|
||||
g = int(h.group(1))
|
||||
k = int(h.group(2))
|
||||
if k == 0:
|
||||
return "снято с поддержки (правки свободны)"
|
||||
if g == 1:
|
||||
return "конфигурация read-only (возможность изменения выключена) — правки невозможны без включения"
|
||||
if not elem_uuid:
|
||||
return "не на поддержке"
|
||||
best = None
|
||||
for m in re.finditer(r"([0-2]),0," + re.escape(elem_uuid.lower()), text):
|
||||
f1 = int(m.group(1))
|
||||
if best is None or f1 < best:
|
||||
best = f1
|
||||
if best is None:
|
||||
return "не на поддержке"
|
||||
return {
|
||||
0: "на замке — прямая правка сломает обновления; дорабатывай через cfe-* либо включи редактирование объекта",
|
||||
1: "редактируется с сохранением поддержки",
|
||||
2: "снято с поддержки (правки свободны)",
|
||||
}.get(best, "не на поддержке")
|
||||
except Exception:
|
||||
return "не на поддержке"
|
||||
|
||||
|
||||
def main():
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
sys.stderr.reconfigure(encoding="utf-8")
|
||||
@@ -278,6 +346,7 @@ def main():
|
||||
parser.add_argument("-Limit", type=int, default=150)
|
||||
parser.add_argument("-Offset", type=int, default=0)
|
||||
parser.add_argument("-OutFile", default=None)
|
||||
parser.add_argument("-Raw", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
# --- Resolve path ---
|
||||
@@ -355,6 +424,7 @@ def main():
|
||||
|
||||
def show_overview():
|
||||
lines.append(f"=== DCS: {template_name} ({total_xml_lines} lines) ===")
|
||||
lines.append(f"Поддержка: {get_support_status_for_path(template_path)}")
|
||||
lines.append("")
|
||||
|
||||
# Sources
|
||||
@@ -634,6 +704,13 @@ def main():
|
||||
sys.exit(1)
|
||||
|
||||
raw_query = unescape_xml("".join(query_node.itertext()))
|
||||
|
||||
# Raw mode: emit verbatim query text only (no headers/TOC/batch split) for round-trip
|
||||
if args.Raw:
|
||||
for ql in raw_query.strip().split("\n"):
|
||||
lines.append(ql.rstrip())
|
||||
return
|
||||
|
||||
ds_name_str = (target_ds.find("s:name", NSMAP).text or "")
|
||||
|
||||
# Split into batches
|
||||
@@ -1719,7 +1796,7 @@ def main():
|
||||
sys.exit(0)
|
||||
result = result[args.Offset:]
|
||||
|
||||
if len(result) > args.Limit:
|
||||
if not args.Raw and len(result) > args.Limit:
|
||||
shown = result[:args.Limit]
|
||||
for line in shown:
|
||||
print(line)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# subsystem-compile v1.5 — Create 1C subsystem from JSON definition
|
||||
# subsystem-compile v1.7 — Create 1C subsystem from JSON definition
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[string]$DefinitionFile,
|
||||
@@ -48,6 +48,126 @@ if (-not [System.IO.Path]::IsPathRooted($OutputDir)) {
|
||||
$OutputDir = Join-Path (Get-Location).Path $OutputDir
|
||||
}
|
||||
|
||||
# --- Support guard (Ext/ParentConfigurations.bin) ---
|
||||
# See docs/1c-support-state-spec.md. Blocks edits of vendor objects "на замке" /
|
||||
# read-only configs unless allowed. Trigger = bin present; reaction from
|
||||
# .v8-project.json editingAllowedCheck (deny|warn|off, default deny). Never
|
||||
# throws — guard errors degrade to allow.
|
||||
function Get-RootUuid([string]$xmlPath) {
|
||||
if (-not (Test-Path $xmlPath)) { return $null }
|
||||
try {
|
||||
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
|
||||
$el = $mx.DocumentElement.FirstChild
|
||||
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
|
||||
if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } }
|
||||
} catch {}
|
||||
return $null
|
||||
}
|
||||
function Find-V8Project([string]$startDir) {
|
||||
$d = $startDir
|
||||
for ($i = 0; $i -lt 20 -and $d; $i++) {
|
||||
$pj = Join-Path $d ".v8-project.json"
|
||||
if (Test-Path $pj) { return $pj }
|
||||
$parent = [System.IO.Path]::GetDirectoryName($d)
|
||||
if ($parent -eq $d) { break }
|
||||
$d = $parent
|
||||
}
|
||||
return $null
|
||||
}
|
||||
function Get-EditMode([string]$cfgDir) {
|
||||
try {
|
||||
$pj = Find-V8Project (Get-Location).Path
|
||||
if (-not $pj) { $pj = Find-V8Project $cfgDir }
|
||||
if (-not $pj) { return 'deny' }
|
||||
$proj = Get-Content -Raw $pj | ConvertFrom-Json
|
||||
$cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/')
|
||||
if ($proj.databases) {
|
||||
foreach ($db in $proj.databases) {
|
||||
if ($db.configSrc) {
|
||||
$src = [System.IO.Path]::GetFullPath($db.configSrc).TrimEnd('\', '/')
|
||||
if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) {
|
||||
if ($db.editingAllowedCheck) { return $db.editingAllowedCheck }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($proj.editingAllowedCheck) { return $proj.editingAllowedCheck }
|
||||
return 'deny'
|
||||
} catch { return 'deny' }
|
||||
}
|
||||
function Assert-EditAllowed([string]$targetPath, [string]$require) {
|
||||
try {
|
||||
$rp = $targetPath
|
||||
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
|
||||
$elemUuid = Get-RootUuid $rp
|
||||
$cfgDir = $null; $binPath = $null
|
||||
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
|
||||
for ($i = 0; $i -lt 12 -and $d; $i++) {
|
||||
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
|
||||
if (-not $cfgDir) {
|
||||
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
|
||||
if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $cfgDir = $d; $binPath = $cand }
|
||||
}
|
||||
if ($elemUuid -and $cfgDir) { break }
|
||||
$parent = [System.IO.Path]::GetDirectoryName($d)
|
||||
if ($parent -eq $d) { break }
|
||||
$d = $parent
|
||||
}
|
||||
# New object (no element file): fall back to config root uuid.
|
||||
if (-not $elemUuid -and $cfgDir) { $elemUuid = Get-RootUuid (Join-Path $cfgDir "Configuration.xml") }
|
||||
if (-not $binPath -or -not (Test-Path $binPath)) { return }
|
||||
$bytes = [System.IO.File]::ReadAllBytes($binPath)
|
||||
if ($bytes.Length -le 32) { return }
|
||||
$start = 0
|
||||
if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 }
|
||||
$text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start)
|
||||
$hm = [regex]::Match($text, '^\{6,(\d+),(\d+),')
|
||||
if (-not $hm.Success) { return }
|
||||
$G = [int]$hm.Groups[1].Value
|
||||
$K = [int]$hm.Groups[2].Value
|
||||
if ($K -eq 0) { return }
|
||||
$best = $null
|
||||
if ($elemUuid) {
|
||||
$u = [regex]::Escape($elemUuid.ToLower())
|
||||
foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) {
|
||||
$f1 = [int]$m.Groups[1].Value
|
||||
if ($null -eq $best -or $f1 -lt $best) { $best = $f1 }
|
||||
}
|
||||
}
|
||||
$blocked = $false; $code = ""; $reason = ""
|
||||
if ($G -eq 1) { $blocked = $true; $code = "capability-off"; $reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)" }
|
||||
elseif ($require -eq 'removed') {
|
||||
if ($null -ne $best -and $best -ne 2) { $blocked = $true; $code = "not-removed"; $reason = "объект не снят с поддержки — удаление сломает обновления" }
|
||||
}
|
||||
else {
|
||||
if ($null -ne $best -and $best -eq 0) { $blocked = $true; $code = "locked"; $reason = "объект на замке — редактирование сломает обновления" }
|
||||
}
|
||||
if (-not $blocked) { return }
|
||||
$mode = Get-EditMode $cfgDir
|
||||
if ($mode -eq 'off') { return }
|
||||
# Use Console.Error (not Write-Error) — under ErrorActionPreference=Stop the
|
||||
# latter throws and would be swallowed by this function's own catch.
|
||||
if ($mode -eq 'warn') { [Console]::Error.WriteLine("[support-guard] ПРЕДУПРЕЖДЕНИЕ: $reason. Цель: $rp"); return }
|
||||
$head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления."
|
||||
$cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются."
|
||||
$offNote = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json."
|
||||
if ($code -eq "capability-off") {
|
||||
$state = "Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «$rp» редактировать нельзя."
|
||||
$fix = "Либо снять защиту явно (навык support-edit, два шага):`n 1. support-edit -Path ""$cfgDir"" -Capability on — включить возможность изменения (объекты пока остаются на замке);`n 2. support-edit -Path ""$rp"" -Set editable — открыть этот объект для редактирования.`n Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора."
|
||||
} elseif ($code -eq "not-removed") {
|
||||
$state = "Состояние: объект «$rp» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора."
|
||||
$fix = "Либо сначала снять объект с поддержки, затем удалять:`n support-edit -Path ""$rp"" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно."
|
||||
} else {
|
||||
$state = "Состояние: объект «$rp» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)."
|
||||
$fix = "Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):`n support-edit -Path ""$rp"" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);`n support-edit -Path ""$rp"" -Set off-support — снять с поддержки: обновления по объекту больше не приходят."
|
||||
}
|
||||
[Console]::Error.WriteLine("$head`n$state`n$cfe`n$fix`n$offNote")
|
||||
exit 1
|
||||
} catch { return }
|
||||
}
|
||||
|
||||
Assert-EditAllowed $OutputDir 'editable'
|
||||
|
||||
# --- 2. XML helpers ---
|
||||
$script:xml = New-Object System.Text.StringBuilder 8192
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# subsystem-compile v1.5 — Create 1C subsystem from JSON definition
|
||||
# subsystem-compile v1.7 — Create 1C subsystem from JSON definition
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
import argparse
|
||||
import json
|
||||
@@ -8,6 +8,166 @@ import re
|
||||
import sys
|
||||
import uuid
|
||||
import xml.etree.ElementTree as ET
|
||||
from lxml import etree
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md
|
||||
# Blocks edits of vendor objects "на замке" / read-only configs. Trigger = bin
|
||||
# present; reaction from .v8-project.json editingAllowedCheck (deny|warn|off,
|
||||
# default deny). Never throws (except sys.exit on deny) — errors degrade to allow.
|
||||
# ============================================================
|
||||
|
||||
def _sg_root_uuid(xml_path):
|
||||
if not os.path.isfile(xml_path):
|
||||
return None
|
||||
try:
|
||||
mx = etree.parse(xml_path).getroot()
|
||||
for child in mx:
|
||||
if isinstance(child.tag, str) and child.get("uuid"):
|
||||
return child.get("uuid")
|
||||
except Exception:
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def _sg_find_v8project(start_dir):
|
||||
d = start_dir
|
||||
for _ in range(20):
|
||||
if not d:
|
||||
break
|
||||
pj = os.path.join(d, ".v8-project.json")
|
||||
if os.path.isfile(pj):
|
||||
return pj
|
||||
parent = os.path.dirname(d)
|
||||
if parent == d:
|
||||
break
|
||||
d = parent
|
||||
return None
|
||||
|
||||
|
||||
def _sg_get_edit_mode(cfg_dir):
|
||||
try:
|
||||
pj = _sg_find_v8project(os.getcwd()) or _sg_find_v8project(cfg_dir)
|
||||
if not pj:
|
||||
return "deny"
|
||||
proj = json.loads(open(pj, encoding="utf-8-sig").read())
|
||||
cfg_full = os.path.normcase(os.path.abspath(cfg_dir)).rstrip("\\/")
|
||||
for db in proj.get("databases", []):
|
||||
src = db.get("configSrc")
|
||||
if src:
|
||||
src_full = os.path.normcase(os.path.abspath(src)).rstrip("\\/")
|
||||
if cfg_full == src_full or cfg_full.startswith(src_full + os.sep):
|
||||
if db.get("editingAllowedCheck"):
|
||||
return db["editingAllowedCheck"]
|
||||
if proj.get("editingAllowedCheck"):
|
||||
return proj["editingAllowedCheck"]
|
||||
return "deny"
|
||||
except Exception:
|
||||
return "deny"
|
||||
|
||||
|
||||
def assert_edit_allowed(target_path, require):
|
||||
try:
|
||||
rp = os.path.abspath(target_path)
|
||||
elem_uuid = _sg_root_uuid(rp)
|
||||
cfg_dir = None
|
||||
bin_path = None
|
||||
d = rp if os.path.isdir(rp) else os.path.dirname(rp)
|
||||
for _ in range(12):
|
||||
if not d:
|
||||
break
|
||||
if not elem_uuid:
|
||||
elem_uuid = _sg_root_uuid(d + ".xml")
|
||||
if not cfg_dir:
|
||||
cand = os.path.join(d, "Ext", "ParentConfigurations.bin")
|
||||
if os.path.exists(cand) or os.path.exists(os.path.join(d, "Configuration.xml")):
|
||||
cfg_dir = d
|
||||
bin_path = cand
|
||||
if elem_uuid and cfg_dir:
|
||||
break
|
||||
parent = os.path.dirname(d)
|
||||
if parent == d:
|
||||
break
|
||||
d = parent
|
||||
if not elem_uuid and cfg_dir:
|
||||
elem_uuid = _sg_root_uuid(os.path.join(cfg_dir, "Configuration.xml"))
|
||||
if not bin_path or not os.path.exists(bin_path):
|
||||
return
|
||||
data = open(bin_path, "rb").read()
|
||||
if len(data) <= 32:
|
||||
return
|
||||
if data[:3] == b"\xef\xbb\xbf":
|
||||
data = data[3:]
|
||||
text = data.decode("utf-8", "replace")
|
||||
h = re.match(r"\{6,(\d+),(\d+),", text)
|
||||
if not h:
|
||||
return
|
||||
g = int(h.group(1))
|
||||
k = int(h.group(2))
|
||||
if k == 0:
|
||||
return
|
||||
best = None
|
||||
if elem_uuid:
|
||||
for m in re.finditer(r"([0-2]),0," + re.escape(elem_uuid.lower()), text):
|
||||
f1 = int(m.group(1))
|
||||
if best is None or f1 < best:
|
||||
best = f1
|
||||
blocked = False
|
||||
code = ""
|
||||
reason = ""
|
||||
if g == 1:
|
||||
blocked = True
|
||||
code = "capability-off"
|
||||
reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)"
|
||||
elif require == "removed":
|
||||
if best is not None and best != 2:
|
||||
blocked = True
|
||||
code = "not-removed"
|
||||
reason = "объект не снят с поддержки — удаление сломает обновления"
|
||||
else:
|
||||
if best is not None and best == 0:
|
||||
blocked = True
|
||||
code = "locked"
|
||||
reason = "объект на замке — редактирование сломает обновления"
|
||||
if not blocked:
|
||||
return
|
||||
mode = _sg_get_edit_mode(cfg_dir)
|
||||
if mode == "off":
|
||||
return
|
||||
if mode == "warn":
|
||||
sys.stderr.write(f"[support-guard] ПРЕДУПРЕЖДЕНИЕ: {reason}. Цель: {rp}\n")
|
||||
return
|
||||
head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления."
|
||||
cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются."
|
||||
off_note = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json."
|
||||
if code == "capability-off":
|
||||
state = f"Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «{rp}» редактировать нельзя."
|
||||
fix = (
|
||||
"Либо снять защиту явно (навык support-edit, два шага):\n"
|
||||
f' 1. support-edit -Path "{cfg_dir}" -Capability on — включить возможность изменения (объекты пока остаются на замке);\n'
|
||||
f' 2. support-edit -Path "{rp}" -Set editable — открыть этот объект для редактирования.\n'
|
||||
" Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора."
|
||||
)
|
||||
elif code == "not-removed":
|
||||
state = f"Состояние: объект «{rp}» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора."
|
||||
fix = (
|
||||
"Либо сначала снять объект с поддержки, затем удалять:\n"
|
||||
f' support-edit -Path "{rp}" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно.'
|
||||
)
|
||||
else:
|
||||
state = f"Состояние: объект «{rp}» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)."
|
||||
fix = (
|
||||
"Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):\n"
|
||||
f' support-edit -Path "{rp}" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);\n'
|
||||
f' support-edit -Path "{rp}" -Set off-support — снять с поддержки: обновления по объекту больше не приходят.'
|
||||
)
|
||||
sys.stderr.write(head + "\n" + state + "\n" + cfe + "\n" + fix + "\n" + off_note + "\n")
|
||||
sys.exit(1)
|
||||
except SystemExit:
|
||||
raise
|
||||
except Exception:
|
||||
return
|
||||
|
||||
|
||||
def detect_format_version(d):
|
||||
@@ -146,6 +306,8 @@ def main():
|
||||
if not os.path.isabs(output_dir):
|
||||
output_dir = os.path.join(os.getcwd(), output_dir)
|
||||
|
||||
assert_edit_allowed(output_dir, "editable")
|
||||
|
||||
# --- 2. Content type normalization (plural→singular, Russian→English) ---
|
||||
CONTENT_TYPE_MAP = {
|
||||
# Plural English → Singular
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# subsystem-edit v1.2 — Edit existing 1C subsystem XML
|
||||
# subsystem-edit v1.5 — Edit existing 1C subsystem XML
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory)][Alias('Path')][string]$SubsystemPath,
|
||||
@@ -121,6 +121,126 @@ if (-not (Test-Path $SubsystemPath)) { Write-Error "File not found: $SubsystemPa
|
||||
$resolvedPath = (Resolve-Path $SubsystemPath).Path
|
||||
$script:resolvedPath = $resolvedPath
|
||||
|
||||
# --- Support guard (Ext/ParentConfigurations.bin) ---
|
||||
# See docs/1c-support-state-spec.md. Blocks edits of vendor objects "на замке" /
|
||||
# read-only configs unless allowed. Trigger = bin present; reaction from
|
||||
# .v8-project.json editingAllowedCheck (deny|warn|off, default deny). Never
|
||||
# throws — guard errors degrade to allow.
|
||||
function Get-RootUuid([string]$xmlPath) {
|
||||
if (-not (Test-Path $xmlPath)) { return $null }
|
||||
try {
|
||||
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
|
||||
$el = $mx.DocumentElement.FirstChild
|
||||
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
|
||||
if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } }
|
||||
} catch {}
|
||||
return $null
|
||||
}
|
||||
function Find-V8Project([string]$startDir) {
|
||||
$d = $startDir
|
||||
for ($i = 0; $i -lt 20 -and $d; $i++) {
|
||||
$pj = Join-Path $d ".v8-project.json"
|
||||
if (Test-Path $pj) { return $pj }
|
||||
$parent = [System.IO.Path]::GetDirectoryName($d)
|
||||
if ($parent -eq $d) { break }
|
||||
$d = $parent
|
||||
}
|
||||
return $null
|
||||
}
|
||||
function Get-EditMode([string]$cfgDir) {
|
||||
try {
|
||||
$pj = Find-V8Project (Get-Location).Path
|
||||
if (-not $pj) { $pj = Find-V8Project $cfgDir }
|
||||
if (-not $pj) { return 'deny' }
|
||||
$proj = Get-Content -Raw $pj | ConvertFrom-Json
|
||||
$cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/')
|
||||
if ($proj.databases) {
|
||||
foreach ($db in $proj.databases) {
|
||||
if ($db.configSrc) {
|
||||
$src = [System.IO.Path]::GetFullPath($db.configSrc).TrimEnd('\', '/')
|
||||
if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) {
|
||||
if ($db.editingAllowedCheck) { return $db.editingAllowedCheck }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($proj.editingAllowedCheck) { return $proj.editingAllowedCheck }
|
||||
return 'deny'
|
||||
} catch { return 'deny' }
|
||||
}
|
||||
function Assert-EditAllowed([string]$targetPath, [string]$require) {
|
||||
try {
|
||||
$rp = $targetPath
|
||||
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
|
||||
$elemUuid = Get-RootUuid $rp
|
||||
$cfgDir = $null; $binPath = $null
|
||||
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
|
||||
for ($i = 0; $i -lt 12 -and $d; $i++) {
|
||||
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
|
||||
if (-not $cfgDir) {
|
||||
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
|
||||
if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $cfgDir = $d; $binPath = $cand }
|
||||
}
|
||||
if ($elemUuid -and $cfgDir) { break }
|
||||
$parent = [System.IO.Path]::GetDirectoryName($d)
|
||||
if ($parent -eq $d) { break }
|
||||
$d = $parent
|
||||
}
|
||||
# New object (no element file): fall back to config root uuid.
|
||||
if (-not $elemUuid -and $cfgDir) { $elemUuid = Get-RootUuid (Join-Path $cfgDir "Configuration.xml") }
|
||||
if (-not $binPath -or -not (Test-Path $binPath)) { return }
|
||||
$bytes = [System.IO.File]::ReadAllBytes($binPath)
|
||||
if ($bytes.Length -le 32) { return }
|
||||
$start = 0
|
||||
if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 }
|
||||
$text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start)
|
||||
$hm = [regex]::Match($text, '^\{6,(\d+),(\d+),')
|
||||
if (-not $hm.Success) { return }
|
||||
$G = [int]$hm.Groups[1].Value
|
||||
$K = [int]$hm.Groups[2].Value
|
||||
if ($K -eq 0) { return }
|
||||
$best = $null
|
||||
if ($elemUuid) {
|
||||
$u = [regex]::Escape($elemUuid.ToLower())
|
||||
foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) {
|
||||
$f1 = [int]$m.Groups[1].Value
|
||||
if ($null -eq $best -or $f1 -lt $best) { $best = $f1 }
|
||||
}
|
||||
}
|
||||
$blocked = $false; $code = ""; $reason = ""
|
||||
if ($G -eq 1) { $blocked = $true; $code = "capability-off"; $reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)" }
|
||||
elseif ($require -eq 'removed') {
|
||||
if ($null -ne $best -and $best -ne 2) { $blocked = $true; $code = "not-removed"; $reason = "объект не снят с поддержки — удаление сломает обновления" }
|
||||
}
|
||||
else {
|
||||
if ($null -ne $best -and $best -eq 0) { $blocked = $true; $code = "locked"; $reason = "объект на замке — редактирование сломает обновления" }
|
||||
}
|
||||
if (-not $blocked) { return }
|
||||
$mode = Get-EditMode $cfgDir
|
||||
if ($mode -eq 'off') { return }
|
||||
# Use Console.Error (not Write-Error) — under ErrorActionPreference=Stop the
|
||||
# latter throws and would be swallowed by this function's own catch.
|
||||
if ($mode -eq 'warn') { [Console]::Error.WriteLine("[support-guard] ПРЕДУПРЕЖДЕНИЕ: $reason. Цель: $rp"); return }
|
||||
$head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления."
|
||||
$cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются."
|
||||
$offNote = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json."
|
||||
if ($code -eq "capability-off") {
|
||||
$state = "Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «$rp» редактировать нельзя."
|
||||
$fix = "Либо снять защиту явно (навык support-edit, два шага):`n 1. support-edit -Path ""$cfgDir"" -Capability on — включить возможность изменения (объекты пока остаются на замке);`n 2. support-edit -Path ""$rp"" -Set editable — открыть этот объект для редактирования.`n Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора."
|
||||
} elseif ($code -eq "not-removed") {
|
||||
$state = "Состояние: объект «$rp» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора."
|
||||
$fix = "Либо сначала снять объект с поддержки, затем удалять:`n support-edit -Path ""$rp"" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно."
|
||||
} else {
|
||||
$state = "Состояние: объект «$rp» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)."
|
||||
$fix = "Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):`n support-edit -Path ""$rp"" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);`n support-edit -Path ""$rp"" -Set off-support — снять с поддержки: обновления по объекту больше не приходят."
|
||||
}
|
||||
[Console]::Error.WriteLine("$head`n$state`n$cfe`n$fix`n$offNote")
|
||||
exit 1
|
||||
} catch { return }
|
||||
}
|
||||
|
||||
Assert-EditAllowed $resolvedPath 'editable'
|
||||
|
||||
# --- Load XML with PreserveWhitespace ---
|
||||
$script:xmlDoc = New-Object System.Xml.XmlDocument
|
||||
$script:xmlDoc.PreserveWhitespace = $true
|
||||
|
||||
@@ -1,16 +1,176 @@
|
||||
#!/usr/bin/env python3
|
||||
# subsystem-edit v1.2 — Edit existing 1C subsystem XML
|
||||
# subsystem-edit v1.5 — Edit existing 1C subsystem XML
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import uuid
|
||||
from lxml import etree
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md
|
||||
# Blocks edits of vendor objects "на замке" / read-only configs. Trigger = bin
|
||||
# present; reaction from .v8-project.json editingAllowedCheck (deny|warn|off,
|
||||
# default deny). Never throws (except sys.exit on deny) — errors degrade to allow.
|
||||
# ============================================================
|
||||
|
||||
def _sg_root_uuid(xml_path):
|
||||
if not os.path.isfile(xml_path):
|
||||
return None
|
||||
try:
|
||||
mx = etree.parse(xml_path).getroot()
|
||||
for child in mx:
|
||||
if isinstance(child.tag, str) and child.get("uuid"):
|
||||
return child.get("uuid")
|
||||
except Exception:
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def _sg_find_v8project(start_dir):
|
||||
d = start_dir
|
||||
for _ in range(20):
|
||||
if not d:
|
||||
break
|
||||
pj = os.path.join(d, ".v8-project.json")
|
||||
if os.path.isfile(pj):
|
||||
return pj
|
||||
parent = os.path.dirname(d)
|
||||
if parent == d:
|
||||
break
|
||||
d = parent
|
||||
return None
|
||||
|
||||
|
||||
def _sg_get_edit_mode(cfg_dir):
|
||||
try:
|
||||
pj = _sg_find_v8project(os.getcwd()) or _sg_find_v8project(cfg_dir)
|
||||
if not pj:
|
||||
return "deny"
|
||||
proj = json.loads(open(pj, encoding="utf-8-sig").read())
|
||||
cfg_full = os.path.normcase(os.path.abspath(cfg_dir)).rstrip("\\/")
|
||||
for db in proj.get("databases", []):
|
||||
src = db.get("configSrc")
|
||||
if src:
|
||||
src_full = os.path.normcase(os.path.abspath(src)).rstrip("\\/")
|
||||
if cfg_full == src_full or cfg_full.startswith(src_full + os.sep):
|
||||
if db.get("editingAllowedCheck"):
|
||||
return db["editingAllowedCheck"]
|
||||
if proj.get("editingAllowedCheck"):
|
||||
return proj["editingAllowedCheck"]
|
||||
return "deny"
|
||||
except Exception:
|
||||
return "deny"
|
||||
|
||||
|
||||
def assert_edit_allowed(target_path, require):
|
||||
try:
|
||||
rp = os.path.abspath(target_path)
|
||||
elem_uuid = _sg_root_uuid(rp)
|
||||
cfg_dir = None
|
||||
bin_path = None
|
||||
d = rp if os.path.isdir(rp) else os.path.dirname(rp)
|
||||
for _ in range(12):
|
||||
if not d:
|
||||
break
|
||||
if not elem_uuid:
|
||||
elem_uuid = _sg_root_uuid(d + ".xml")
|
||||
if not cfg_dir:
|
||||
cand = os.path.join(d, "Ext", "ParentConfigurations.bin")
|
||||
if os.path.exists(cand) or os.path.exists(os.path.join(d, "Configuration.xml")):
|
||||
cfg_dir = d
|
||||
bin_path = cand
|
||||
if elem_uuid and cfg_dir:
|
||||
break
|
||||
parent = os.path.dirname(d)
|
||||
if parent == d:
|
||||
break
|
||||
d = parent
|
||||
if not elem_uuid and cfg_dir:
|
||||
elem_uuid = _sg_root_uuid(os.path.join(cfg_dir, "Configuration.xml"))
|
||||
if not bin_path or not os.path.exists(bin_path):
|
||||
return
|
||||
data = open(bin_path, "rb").read()
|
||||
if len(data) <= 32:
|
||||
return
|
||||
if data[:3] == b"\xef\xbb\xbf":
|
||||
data = data[3:]
|
||||
text = data.decode("utf-8", "replace")
|
||||
h = re.match(r"\{6,(\d+),(\d+),", text)
|
||||
if not h:
|
||||
return
|
||||
g = int(h.group(1))
|
||||
k = int(h.group(2))
|
||||
if k == 0:
|
||||
return
|
||||
best = None
|
||||
if elem_uuid:
|
||||
for m in re.finditer(r"([0-2]),0," + re.escape(elem_uuid.lower()), text):
|
||||
f1 = int(m.group(1))
|
||||
if best is None or f1 < best:
|
||||
best = f1
|
||||
blocked = False
|
||||
code = ""
|
||||
reason = ""
|
||||
if g == 1:
|
||||
blocked = True
|
||||
code = "capability-off"
|
||||
reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)"
|
||||
elif require == "removed":
|
||||
if best is not None and best != 2:
|
||||
blocked = True
|
||||
code = "not-removed"
|
||||
reason = "объект не снят с поддержки — удаление сломает обновления"
|
||||
else:
|
||||
if best is not None and best == 0:
|
||||
blocked = True
|
||||
code = "locked"
|
||||
reason = "объект на замке — редактирование сломает обновления"
|
||||
if not blocked:
|
||||
return
|
||||
mode = _sg_get_edit_mode(cfg_dir)
|
||||
if mode == "off":
|
||||
return
|
||||
if mode == "warn":
|
||||
sys.stderr.write(f"[support-guard] ПРЕДУПРЕЖДЕНИЕ: {reason}. Цель: {rp}\n")
|
||||
return
|
||||
head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления."
|
||||
cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются."
|
||||
off_note = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json."
|
||||
if code == "capability-off":
|
||||
state = f"Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «{rp}» редактировать нельзя."
|
||||
fix = (
|
||||
"Либо снять защиту явно (навык support-edit, два шага):\n"
|
||||
f' 1. support-edit -Path "{cfg_dir}" -Capability on — включить возможность изменения (объекты пока остаются на замке);\n'
|
||||
f' 2. support-edit -Path "{rp}" -Set editable — открыть этот объект для редактирования.\n'
|
||||
" Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора."
|
||||
)
|
||||
elif code == "not-removed":
|
||||
state = f"Состояние: объект «{rp}» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора."
|
||||
fix = (
|
||||
"Либо сначала снять объект с поддержки, затем удалять:\n"
|
||||
f' support-edit -Path "{rp}" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно.'
|
||||
)
|
||||
else:
|
||||
state = f"Состояние: объект «{rp}» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)."
|
||||
fix = (
|
||||
"Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):\n"
|
||||
f' support-edit -Path "{rp}" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);\n'
|
||||
f' support-edit -Path "{rp}" -Set off-support — снять с поддержки: обновления по объекту больше не приходят.'
|
||||
)
|
||||
sys.stderr.write(head + "\n" + state + "\n" + cfe + "\n" + fix + "\n" + off_note + "\n")
|
||||
sys.exit(1)
|
||||
except SystemExit:
|
||||
raise
|
||||
except Exception:
|
||||
return
|
||||
|
||||
|
||||
def new_uuid():
|
||||
return str(uuid.uuid4())
|
||||
|
||||
@@ -316,6 +476,8 @@ def main():
|
||||
|
||||
resolved_path = os.path.abspath(subsystem_path)
|
||||
|
||||
assert_edit_allowed(resolved_path, "editable")
|
||||
|
||||
# --- Load XML ---
|
||||
xml_parser = etree.XMLParser(remove_blank_text=False)
|
||||
tree = etree.parse(resolved_path, xml_parser)
|
||||
@@ -602,7 +764,7 @@ def main():
|
||||
if os.path.isfile(validate_script):
|
||||
print()
|
||||
print("--- Running subsystem-validate ---")
|
||||
subprocess.run([sys.executable, validate_script, "-SubsystemPath", "-Path", resolved_path])
|
||||
subprocess.run([sys.executable, validate_script, "-SubsystemPath", resolved_path])
|
||||
|
||||
# --- Summary ---
|
||||
print()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# subsystem-info v1.0 — Compact summary of 1C subsystem structure
|
||||
# subsystem-info v1.1 — Compact summary of 1C subsystem structure
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][Alias('Path')][string]$SubsystemPath,
|
||||
@@ -17,6 +17,65 @@ $ErrorActionPreference = 'Stop'
|
||||
$script:lines = @()
|
||||
function Out([string]$text) { $script:lines += $text }
|
||||
|
||||
function Get-SupportStatusForPath([string]$targetPath) {
|
||||
try {
|
||||
$rp = (Resolve-Path $targetPath).Path
|
||||
$elemUuid = $null
|
||||
$binPath = $null
|
||||
# Reads the uuid of the first metadata element in an .xml file (or $null).
|
||||
function Get-RootUuid([string]$xmlPath) {
|
||||
if (-not (Test-Path $xmlPath)) { return $null }
|
||||
try {
|
||||
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
|
||||
$el = $mx.DocumentElement.FirstChild
|
||||
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
|
||||
if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } }
|
||||
} catch {}
|
||||
return $null
|
||||
}
|
||||
# The target file itself may be the element meta-xml (e.g. Subsystems/X.xml).
|
||||
$elemUuid = Get-RootUuid $rp
|
||||
$d = [System.IO.Path]::GetDirectoryName($rp)
|
||||
for ($i = 0; $i -lt 12 -and $d; $i++) {
|
||||
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
|
||||
if (-not $binPath) {
|
||||
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
|
||||
if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $binPath = $cand }
|
||||
}
|
||||
if ($elemUuid -and $binPath) { break }
|
||||
$parent = [System.IO.Path]::GetDirectoryName($d)
|
||||
if ($parent -eq $d) { break }
|
||||
$d = $parent
|
||||
}
|
||||
if (-not $binPath -or -not (Test-Path $binPath)) { return "не на поддержке" }
|
||||
$bytes = [System.IO.File]::ReadAllBytes($binPath)
|
||||
if ($bytes.Length -le 32) { return "снято с поддержки (правки свободны)" }
|
||||
$start = 0
|
||||
if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 }
|
||||
$text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start)
|
||||
$h = [regex]::Match($text, '^\{6,(\d+),(\d+),')
|
||||
if (-not $h.Success) { return "не на поддержке" }
|
||||
$G = [int]$h.Groups[1].Value
|
||||
$K = [int]$h.Groups[2].Value
|
||||
if ($K -eq 0) { return "снято с поддержки (правки свободны)" }
|
||||
if ($G -eq 1) { return "конфигурация read-only (возможность изменения выключена) — правки невозможны без включения" }
|
||||
if (-not $elemUuid) { return "не на поддержке" }
|
||||
$u = [regex]::Escape($elemUuid.ToLower())
|
||||
$best = $null
|
||||
foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) {
|
||||
$f1 = [int]$m.Groups[1].Value
|
||||
if ($null -eq $best -or $f1 -lt $best) { $best = $f1 }
|
||||
}
|
||||
if ($null -eq $best) { return "не на поддержке" }
|
||||
switch ($best) {
|
||||
0 { return "на замке — прямая правка сломает обновления; дорабатывай через cfe-* либо включи редактирование объекта" }
|
||||
1 { return "редактируется с сохранением поддержки" }
|
||||
2 { return "снято с поддержки (правки свободны)" }
|
||||
}
|
||||
return "не на поддержке"
|
||||
} catch { return "не на поддержке" }
|
||||
}
|
||||
|
||||
# --- Resolve path ---
|
||||
if (-not [System.IO.Path]::IsPathRooted($SubsystemPath)) {
|
||||
$SubsystemPath = Join-Path (Get-Location).Path $SubsystemPath
|
||||
@@ -116,6 +175,7 @@ function Get-SubsystemDir([string]$xmlPath) {
|
||||
# --- Show functions for full mode ---
|
||||
function Show-Overview {
|
||||
Out "Подсистема: $subName"
|
||||
Out "Поддержка: $(Get-SupportStatusForPath $SubsystemPath)"
|
||||
if ($synonym -and $synonym -ne $subName) { Out "Синоним: $synonym" }
|
||||
if ($commentText) { Out "Комментарий: $commentText" }
|
||||
Out "ВключатьВКомандныйИнтерфейс: $inclCI"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user