mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-09-03 00:30:52 +03:00
feat(db-repo): py-порт, реестр семей копий и подсказки в db-load-git
Порт .py зеркалит .ps1 по порядку функций, именам и комментариям — расхождения только там, где их диктует рантайм. Общие функции взяты из соседних портов дословно. Копии зарегистрированы в check-inline-drift.mjs: db-repo присоединён к шести платформенным семьям, четыре функции блока реквизитов хранилища заведены новыми семьями, разбор сообщений хранилища — семьёй с эталоном db-load-xml. Гард сразу нашёл настоящее расхождение: копии Get-RepositoryArgs в четырёх соседях остались со старым comma-return, эталон правился позже. Ресинхронизировано. Разбор сообщений хранилища добавлен и в db-load-git: он тоже грузит в базу частично и получает те же три отказа платформы. Подкоманда стала именованной (-Command): в группе скрипты принимают только именованные параметры, слэш-форму в них переводит модель. Позиционный параметр у пишущего навыка запрещён гардом check-positional-binding. Mandatory при этом не ставим — обязательный параметр PowerShell запрашивает интерактивно, а в пакетном запуске это зависание. Все девять гардов и функциональные тесты db-* зелёные в обоих рантаймах. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
012f5c3c30
commit
1327b4fc5a
@@ -204,7 +204,7 @@ function Get-RepositoryArgs {
|
||||
$a += "/ConfigurationRepositoryF`"$($Repo.Path)`""
|
||||
if ($Repo.User) { $a += "/ConfigurationRepositoryN`"$($Repo.User)`"" }
|
||||
if ($Repo.Password) { $a += "/ConfigurationRepositoryP`"$($Repo.Password)`"" }
|
||||
return ,$a
|
||||
return $a
|
||||
}
|
||||
|
||||
function Protect-Secrets {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# db-dump-xml v1.16 — Dump 1C configuration to XML files
|
||||
# db-dump-xml v1.17 — Dump 1C configuration to XML files
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
@@ -72,10 +72,95 @@ IBCMD_OWNED_KEYS = [
|
||||
"--import", "--export", "--apply", "--force", "--create-database",
|
||||
"--user", "--password",
|
||||
]
|
||||
V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP"]
|
||||
V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP", "/ConfigurationRepositoryP"]
|
||||
IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"]
|
||||
|
||||
|
||||
# --- Реквизиты хранилища из .v8-project.json ---
|
||||
# Модель их не передаёт: скрипт сопоставляет параметры соединения с записью в databases[]
|
||||
# и берёт repository оттуда. Тот же приём, что в cf-edit.py (сопоставление по configSrc).
|
||||
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 same_path(a, b):
|
||||
if not a or not b:
|
||||
return False
|
||||
try:
|
||||
return os.path.abspath(a).rstrip("\\/").lower() == os.path.abspath(b).rstrip("\\/").lower()
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def find_project_database(args):
|
||||
"""Запись базы в реестре, соответствующая переданному соединению. None, если не найдена."""
|
||||
pf = _sg_find_v8project(os.getcwd())
|
||||
if not pf:
|
||||
return None
|
||||
try:
|
||||
with open(pf, encoding="utf-8-sig") as f:
|
||||
proj = json.load(f)
|
||||
except Exception:
|
||||
return None
|
||||
for db in proj.get("databases") or []:
|
||||
if args.InfoBasePath and db.get("path") and same_path(db["path"], args.InfoBasePath):
|
||||
return db
|
||||
if args.InfoBaseServer and args.InfoBaseRef and db.get("server") and db.get("ref"):
|
||||
if (db["server"].lower() == args.InfoBaseServer.lower()
|
||||
and db["ref"].lower() == args.InfoBaseRef.lower()):
|
||||
return db
|
||||
return None
|
||||
|
||||
|
||||
def resolve_repository_settings(args):
|
||||
"""Возвращает dict path/user/password/from_registry. Явные -Repository* сильнее реестра."""
|
||||
db_rec = find_project_database(args)
|
||||
rec = None
|
||||
if db_rec:
|
||||
if args.Extension:
|
||||
# У расширения СВОЁ хранилище со своим путём (проверено): выбирается парой
|
||||
# /ConfigurationRepositoryF"<путь расширения>" + -Extension "<Имя>".
|
||||
for ext in db_rec.get("extensions") or []:
|
||||
if (ext.get("name") or "").lower() == args.Extension.lower():
|
||||
rec = ext.get("repository")
|
||||
break
|
||||
else:
|
||||
rec = db_rec.get("repository")
|
||||
path = args.RepositoryPath or ((rec or {}).get("path") or None)
|
||||
user = args.RepositoryUser or ((rec or {}).get("user") or None)
|
||||
# Пустой пароль = отсутствующий: 1С требует опускать ключ целиком, а не передавать пустое значение.
|
||||
pwd = args.RepositoryPassword or ((rec or {}).get("password") or None)
|
||||
return {
|
||||
"path": path.strip().strip('"') if path else None,
|
||||
"user": user,
|
||||
"password": pwd,
|
||||
"from_registry": bool(rec and rec.get("path")),
|
||||
}
|
||||
|
||||
|
||||
def repository_args(repo):
|
||||
"""Ключи доступа к хранилищу. Форма — кавычки ВНУТРИ токена, как у /N и /P."""
|
||||
a = []
|
||||
if not repo or not repo.get("path"):
|
||||
return a
|
||||
a.append('/ConfigurationRepositoryF"%s"' % repo["path"])
|
||||
if repo.get("user"):
|
||||
a.append('/ConfigurationRepositoryN"%s"' % repo["user"])
|
||||
if repo.get("password"):
|
||||
a.append('/ConfigurationRepositoryP"%s"' % repo["password"])
|
||||
return a
|
||||
|
||||
|
||||
def arg_key_match(token, key):
|
||||
"""Token matches a key when it equals it, or starts with it and the next character
|
||||
is not a letter — catches glued /N"user" and --password=x, while keeping
|
||||
@@ -388,6 +473,9 @@ def main():
|
||||
parser.add_argument("-InfoBaseRef", default="", help="Infobase name on server")
|
||||
parser.add_argument("-UserName", default="", help="1C user name")
|
||||
parser.add_argument("-Password", default="", help="1C user password")
|
||||
parser.add_argument("-RepositoryPath", default="")
|
||||
parser.add_argument("-RepositoryUser", default="")
|
||||
parser.add_argument("-RepositoryPassword", default="")
|
||||
parser.add_argument("-ConfigDir", required=True, help="Directory for configuration dump")
|
||||
parser.add_argument(
|
||||
"-Mode",
|
||||
@@ -396,6 +484,7 @@ def main():
|
||||
help="Dump mode (default: Changes)",
|
||||
)
|
||||
parser.add_argument("-Objects", default="", help="Comma-separated metadata object names (for Partial mode)")
|
||||
parser.add_argument("-ObjectsFile", default="")
|
||||
parser.add_argument("-Extension", default="", help="Extension name to dump")
|
||||
parser.add_argument("-AllExtensions", action="store_true", help="Dump all extensions")
|
||||
parser.add_argument(
|
||||
@@ -443,8 +532,19 @@ def main():
|
||||
sys.exit(1)
|
||||
|
||||
# --- Validate Partial mode ---
|
||||
# Список объектов приходит либо строкой, либо файлом: файл нужен, чтобы не перепечатывать
|
||||
# то, что уже напечатал другой навык (например /db-repo update со списком полученных объектов).
|
||||
if args.ObjectsFile:
|
||||
if not os.path.exists(args.ObjectsFile):
|
||||
print("Error: -ObjectsFile not found: %s" % args.ObjectsFile, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
with open(args.ObjectsFile, encoding="utf-8-sig") as f:
|
||||
from_file = [s.strip() for s in f.read().splitlines()
|
||||
if s.strip() and not s.strip().startswith("#")]
|
||||
inline = [s.strip() for s in args.Objects.split(",") if s.strip()]
|
||||
args.Objects = ",".join(inline + from_file)
|
||||
if args.Mode == "Partial" and not args.Objects:
|
||||
print("Error: -Objects required for Partial mode", file=sys.stderr)
|
||||
print("Error: -Objects or -ObjectsFile required for Partial mode", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# --- Create output dir if needed ---
|
||||
@@ -513,6 +613,11 @@ def main():
|
||||
if args.Password:
|
||||
arguments.append(f'/P"{args.Password}"')
|
||||
|
||||
# База под хранилищем не примет НИ ОДНОЙ операции конфигуратора без этих реквизитов, а для
|
||||
# базы вне хранилища они безвредны — поэтому подставляем всегда, когда они известны.
|
||||
repo = resolve_repository_settings(args)
|
||||
arguments.extend(repository_args(repo))
|
||||
|
||||
arguments += ["/DumpConfigToFiles", f'"{args.ConfigDir}"']
|
||||
arguments += ["-Format", args.Format]
|
||||
|
||||
@@ -551,7 +656,7 @@ def main():
|
||||
arguments.extend(quote_if_needed(a) for a in extra_args)
|
||||
|
||||
# --- Execute ---
|
||||
print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
|
||||
print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName, repo['password'])}")
|
||||
result = run_v8(v8path, arguments)
|
||||
exit_code = result.returncode
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# db-load-git v1.22 — Load Git changes into 1C database
|
||||
# db-load-git v1.23 — Load Git changes into 1C database
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||
<#
|
||||
@@ -216,7 +216,31 @@ function Get-RepositoryArgs {
|
||||
$a += "/ConfigurationRepositoryF`"$($Repo.Path)`""
|
||||
if ($Repo.User) { $a += "/ConfigurationRepositoryN`"$($Repo.User)`"" }
|
||||
if ($Repo.Password) { $a += "/ConfigurationRepositoryP`"$($Repo.Password)`"" }
|
||||
return ,$a
|
||||
return $a
|
||||
}
|
||||
|
||||
# Сообщения платформы про хранилище конфигурации называют причину, но не действие. Действие
|
||||
# дописываем сами: без него модель упирается в отказ и не знает, чем его лечить.
|
||||
function Write-RepositoryHints {
|
||||
param([string]$LogText)
|
||||
if (-not $LogText) { return }
|
||||
if ($LogText -match 'текущая конфигурация помещена в хранилище') {
|
||||
Write-Host "[hint] полная загрузка в базу, подключённую к хранилищу, невозможна." -ForegroundColor Yellow
|
||||
Write-Host " Используйте -Mode Partial, предварительно захватив объекты: /db-repo lock" -ForegroundColor Yellow
|
||||
}
|
||||
foreach ($m in [regex]::Matches($LogText, 'объект метаданных ([^\s]+) не захвачен в хранилище')) {
|
||||
$obj = $m.Groups[1].Value
|
||||
if ($obj -eq 'Configuration') {
|
||||
Write-Host "[hint] не захвачен корень конфигурации — он нужен, чтобы добавить или удалить объект:" -ForegroundColor Yellow
|
||||
Write-Host " /db-repo lock <база> -Objects `"Конфигурация`"" -ForegroundColor Yellow
|
||||
} else {
|
||||
Write-Host "[hint] объект не захвачен в хранилище: /db-repo lock <база> -Objects `"$obj`"" -ForegroundColor Yellow
|
||||
}
|
||||
}
|
||||
if ($LogText -match 'Соединение с хранилищем конфигурации не установлено') {
|
||||
Write-Host "[hint] база подключена к хранилищу, но его реквизиты неизвестны." -ForegroundColor Yellow
|
||||
Write-Host " Добавьте `"repository`" в запись базы в .v8-project.json (см. /db-list)." -ForegroundColor Yellow
|
||||
}
|
||||
}
|
||||
|
||||
function Protect-Secrets {
|
||||
@@ -816,6 +840,7 @@ try {
|
||||
}
|
||||
}
|
||||
Write-PlatformOutput $__v8.Output
|
||||
Write-RepositoryHints $logContent
|
||||
|
||||
# Причину не называем: строки лога печатаются следом и говорят за себя, а класс проблемы
|
||||
# разный — от отброшенного свойства до нерабочей на этой платформе конфигурации. Подсказку
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# db-load-git v1.21 — Load Git changes into 1C database
|
||||
# db-load-git v1.23 — Load Git changes into 1C database
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
@@ -72,10 +72,115 @@ IBCMD_OWNED_KEYS = [
|
||||
"--import", "--export", "--apply", "--force", "--create-database",
|
||||
"--user", "--password",
|
||||
]
|
||||
V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP"]
|
||||
V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP", "/ConfigurationRepositoryP"]
|
||||
IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"]
|
||||
|
||||
|
||||
# --- Реквизиты хранилища из .v8-project.json ---
|
||||
# Модель их не передаёт: скрипт сопоставляет параметры соединения с записью в databases[]
|
||||
# и берёт repository оттуда. Тот же приём, что в cf-edit.py (сопоставление по configSrc).
|
||||
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 same_path(a, b):
|
||||
if not a or not b:
|
||||
return False
|
||||
try:
|
||||
return os.path.abspath(a).rstrip("\\/").lower() == os.path.abspath(b).rstrip("\\/").lower()
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def find_project_database(args):
|
||||
"""Запись базы в реестре, соответствующая переданному соединению. None, если не найдена."""
|
||||
pf = _sg_find_v8project(os.getcwd())
|
||||
if not pf:
|
||||
return None
|
||||
try:
|
||||
with open(pf, encoding="utf-8-sig") as f:
|
||||
proj = json.load(f)
|
||||
except Exception:
|
||||
return None
|
||||
for db in proj.get("databases") or []:
|
||||
if args.InfoBasePath and db.get("path") and same_path(db["path"], args.InfoBasePath):
|
||||
return db
|
||||
if args.InfoBaseServer and args.InfoBaseRef and db.get("server") and db.get("ref"):
|
||||
if (db["server"].lower() == args.InfoBaseServer.lower()
|
||||
and db["ref"].lower() == args.InfoBaseRef.lower()):
|
||||
return db
|
||||
return None
|
||||
|
||||
|
||||
def resolve_repository_settings(args):
|
||||
"""Возвращает dict path/user/password/from_registry. Явные -Repository* сильнее реестра."""
|
||||
db_rec = find_project_database(args)
|
||||
rec = None
|
||||
if db_rec:
|
||||
if args.Extension:
|
||||
# У расширения СВОЁ хранилище со своим путём (проверено): выбирается парой
|
||||
# /ConfigurationRepositoryF"<путь расширения>" + -Extension "<Имя>".
|
||||
for ext in db_rec.get("extensions") or []:
|
||||
if (ext.get("name") or "").lower() == args.Extension.lower():
|
||||
rec = ext.get("repository")
|
||||
break
|
||||
else:
|
||||
rec = db_rec.get("repository")
|
||||
path = args.RepositoryPath or ((rec or {}).get("path") or None)
|
||||
user = args.RepositoryUser or ((rec or {}).get("user") or None)
|
||||
# Пустой пароль = отсутствующий: 1С требует опускать ключ целиком, а не передавать пустое значение.
|
||||
pwd = args.RepositoryPassword or ((rec or {}).get("password") or None)
|
||||
return {
|
||||
"path": path.strip().strip('"') if path else None,
|
||||
"user": user,
|
||||
"password": pwd,
|
||||
"from_registry": bool(rec and rec.get("path")),
|
||||
}
|
||||
|
||||
|
||||
def repository_args(repo):
|
||||
"""Ключи доступа к хранилищу. Форма — кавычки ВНУТРИ токена, как у /N и /P."""
|
||||
a = []
|
||||
if not repo or not repo.get("path"):
|
||||
return a
|
||||
a.append('/ConfigurationRepositoryF"%s"' % repo["path"])
|
||||
if repo.get("user"):
|
||||
a.append('/ConfigurationRepositoryN"%s"' % repo["user"])
|
||||
if repo.get("password"):
|
||||
a.append('/ConfigurationRepositoryP"%s"' % repo["password"])
|
||||
return a
|
||||
|
||||
|
||||
# Сообщения платформы про хранилище конфигурации называют причину, но не действие. Действие
|
||||
# дописываем сами: без него модель упирается в отказ и не знает, чем его лечить.
|
||||
def write_repository_hints(log_text):
|
||||
if not log_text:
|
||||
return
|
||||
if "текущая конфигурация помещена в хранилище" in log_text:
|
||||
print("[hint] полная загрузка в базу, подключённую к хранилищу, невозможна.")
|
||||
print(" Используйте -Mode Partial, предварительно захватив объекты: /db-repo lock")
|
||||
for m in re.finditer(r"объект метаданных (\S+) не захвачен в хранилище", log_text):
|
||||
obj = m.group(1)
|
||||
if obj == "Configuration":
|
||||
print("[hint] не захвачен корень конфигурации — он нужен, чтобы добавить или удалить объект:")
|
||||
print(' /db-repo lock <база> -Objects "Конфигурация"')
|
||||
else:
|
||||
print('[hint] объект не захвачен в хранилище: /db-repo lock <база> -Objects "%s"' % obj)
|
||||
if "Соединение с хранилищем конфигурации не установлено" in log_text:
|
||||
print("[hint] база подключена к хранилищу, но его реквизиты неизвестны.")
|
||||
print(' Добавьте "repository" в запись базы в .v8-project.json (см. /db-list).')
|
||||
|
||||
|
||||
def arg_key_match(token, key):
|
||||
"""Token matches a key when it equals it, or starts with it and the next character
|
||||
is not a letter — catches glued /N"user" and --password=x, while keeping
|
||||
@@ -460,6 +565,9 @@ def main():
|
||||
parser.add_argument("-InfoBaseRef", default="", help="Infobase name on server")
|
||||
parser.add_argument("-UserName", default="", help="1C user name")
|
||||
parser.add_argument("-Password", default="", help="1C user password")
|
||||
parser.add_argument("-RepositoryPath", default="")
|
||||
parser.add_argument("-RepositoryUser", default="")
|
||||
parser.add_argument("-RepositoryPassword", default="")
|
||||
parser.add_argument("-ConfigDir", required=True, help="Directory with XML configuration (git repo)")
|
||||
parser.add_argument(
|
||||
"-Source",
|
||||
@@ -704,6 +812,11 @@ def main():
|
||||
if args.Password:
|
||||
arguments.append(f'/P"{args.Password}"')
|
||||
|
||||
# База под хранилищем не примет НИ ОДНОЙ операции конфигуратора без этих реквизитов, а для
|
||||
# базы вне хранилища они безвредны — поэтому подставляем всегда, когда они известны.
|
||||
repo = resolve_repository_settings(args)
|
||||
arguments.extend(repository_args(repo))
|
||||
|
||||
arguments += ["/LoadConfigFromFiles", f'"{args.ConfigDir}"']
|
||||
arguments += ["-listFile", f'"{list_file}"']
|
||||
arguments += ["-Format", args.Format]
|
||||
@@ -729,7 +842,7 @@ def main():
|
||||
# --- Execute ---
|
||||
print("")
|
||||
print("Executing partial configuration load...")
|
||||
print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
|
||||
print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName, repo['password'])}")
|
||||
|
||||
result = run_v8(v8path, arguments)
|
||||
exit_code = result.returncode
|
||||
@@ -754,6 +867,7 @@ def main():
|
||||
pass
|
||||
|
||||
print_platform_output(result)
|
||||
write_repository_hints(log_content)
|
||||
|
||||
# Причину не называем: строки лога печатаются следом и говорят за себя, а класс проблемы
|
||||
# разный — от отброшенного свойства до нерабочей на этой платформе конфигурации. Подсказку
|
||||
|
||||
@@ -210,7 +210,7 @@ function Get-RepositoryArgs {
|
||||
$a += "/ConfigurationRepositoryF`"$($Repo.Path)`""
|
||||
if ($Repo.User) { $a += "/ConfigurationRepositoryN`"$($Repo.User)`"" }
|
||||
if ($Repo.Password) { $a += "/ConfigurationRepositoryP`"$($Repo.Password)`"" }
|
||||
return ,$a
|
||||
return $a
|
||||
}
|
||||
|
||||
# Сообщения платформы про хранилище конфигурации называют причину, но не действие. Действие
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# db-load-xml v1.22 — Load 1C configuration from XML files
|
||||
# db-load-xml v1.23 — Load 1C configuration from XML files
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
@@ -72,10 +72,115 @@ IBCMD_OWNED_KEYS = [
|
||||
"--import", "--export", "--apply", "--force", "--create-database",
|
||||
"--user", "--password",
|
||||
]
|
||||
V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP"]
|
||||
V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP", "/ConfigurationRepositoryP"]
|
||||
IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"]
|
||||
|
||||
|
||||
# --- Реквизиты хранилища из .v8-project.json ---
|
||||
# Модель их не передаёт: скрипт сопоставляет параметры соединения с записью в databases[]
|
||||
# и берёт repository оттуда. Тот же приём, что в cf-edit.py (сопоставление по configSrc).
|
||||
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 same_path(a, b):
|
||||
if not a or not b:
|
||||
return False
|
||||
try:
|
||||
return os.path.abspath(a).rstrip("\\/").lower() == os.path.abspath(b).rstrip("\\/").lower()
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def find_project_database(args):
|
||||
"""Запись базы в реестре, соответствующая переданному соединению. None, если не найдена."""
|
||||
pf = _sg_find_v8project(os.getcwd())
|
||||
if not pf:
|
||||
return None
|
||||
try:
|
||||
with open(pf, encoding="utf-8-sig") as f:
|
||||
proj = json.load(f)
|
||||
except Exception:
|
||||
return None
|
||||
for db in proj.get("databases") or []:
|
||||
if args.InfoBasePath and db.get("path") and same_path(db["path"], args.InfoBasePath):
|
||||
return db
|
||||
if args.InfoBaseServer and args.InfoBaseRef and db.get("server") and db.get("ref"):
|
||||
if (db["server"].lower() == args.InfoBaseServer.lower()
|
||||
and db["ref"].lower() == args.InfoBaseRef.lower()):
|
||||
return db
|
||||
return None
|
||||
|
||||
|
||||
def resolve_repository_settings(args):
|
||||
"""Возвращает dict path/user/password/from_registry. Явные -Repository* сильнее реестра."""
|
||||
db_rec = find_project_database(args)
|
||||
rec = None
|
||||
if db_rec:
|
||||
if args.Extension:
|
||||
# У расширения СВОЁ хранилище со своим путём (проверено): выбирается парой
|
||||
# /ConfigurationRepositoryF"<путь расширения>" + -Extension "<Имя>".
|
||||
for ext in db_rec.get("extensions") or []:
|
||||
if (ext.get("name") or "").lower() == args.Extension.lower():
|
||||
rec = ext.get("repository")
|
||||
break
|
||||
else:
|
||||
rec = db_rec.get("repository")
|
||||
path = args.RepositoryPath or ((rec or {}).get("path") or None)
|
||||
user = args.RepositoryUser or ((rec or {}).get("user") or None)
|
||||
# Пустой пароль = отсутствующий: 1С требует опускать ключ целиком, а не передавать пустое значение.
|
||||
pwd = args.RepositoryPassword or ((rec or {}).get("password") or None)
|
||||
return {
|
||||
"path": path.strip().strip('"') if path else None,
|
||||
"user": user,
|
||||
"password": pwd,
|
||||
"from_registry": bool(rec and rec.get("path")),
|
||||
}
|
||||
|
||||
|
||||
def repository_args(repo):
|
||||
"""Ключи доступа к хранилищу. Форма — кавычки ВНУТРИ токена, как у /N и /P."""
|
||||
a = []
|
||||
if not repo or not repo.get("path"):
|
||||
return a
|
||||
a.append('/ConfigurationRepositoryF"%s"' % repo["path"])
|
||||
if repo.get("user"):
|
||||
a.append('/ConfigurationRepositoryN"%s"' % repo["user"])
|
||||
if repo.get("password"):
|
||||
a.append('/ConfigurationRepositoryP"%s"' % repo["password"])
|
||||
return a
|
||||
|
||||
|
||||
# Сообщения платформы про хранилище конфигурации называют причину, но не действие. Действие
|
||||
# дописываем сами: без него модель упирается в отказ и не знает, чем его лечить.
|
||||
def write_repository_hints(log_text):
|
||||
if not log_text:
|
||||
return
|
||||
if "текущая конфигурация помещена в хранилище" in log_text:
|
||||
print("[hint] полная загрузка в базу, подключённую к хранилищу, невозможна.")
|
||||
print(" Используйте -Mode Partial, предварительно захватив объекты: /db-repo lock")
|
||||
for m in re.finditer(r"объект метаданных (\S+) не захвачен в хранилище", log_text):
|
||||
obj = m.group(1)
|
||||
if obj == "Configuration":
|
||||
print("[hint] не захвачен корень конфигурации — он нужен, чтобы добавить или удалить объект:")
|
||||
print(' /db-repo lock <база> -Objects "Конфигурация"')
|
||||
else:
|
||||
print('[hint] объект не захвачен в хранилище: /db-repo lock <база> -Objects "%s"' % obj)
|
||||
if "Соединение с хранилищем конфигурации не установлено" in log_text:
|
||||
print("[hint] база подключена к хранилищу, но его реквизиты неизвестны.")
|
||||
print(' Добавьте "repository" в запись базы в .v8-project.json (см. /db-list).')
|
||||
|
||||
|
||||
def arg_key_match(token, key):
|
||||
"""Token matches a key when it equals it, or starts with it and the next character
|
||||
is not a letter — catches glued /N"user" and --password=x, while keeping
|
||||
@@ -438,6 +543,9 @@ def main():
|
||||
parser.add_argument("-InfoBaseRef", default="", help="Infobase name on server")
|
||||
parser.add_argument("-UserName", default="", help="1C user name")
|
||||
parser.add_argument("-Password", default="", help="1C user password")
|
||||
parser.add_argument("-RepositoryPath", default="")
|
||||
parser.add_argument("-RepositoryUser", default="")
|
||||
parser.add_argument("-RepositoryPassword", default="")
|
||||
parser.add_argument("-ConfigDir", required=True, help="Directory with XML configuration sources")
|
||||
parser.add_argument(
|
||||
"-Mode",
|
||||
@@ -593,6 +701,11 @@ def main():
|
||||
if args.Password:
|
||||
arguments.append(f'/P"{args.Password}"')
|
||||
|
||||
# База под хранилищем не примет НИ ОДНОЙ операции конфигуратора без этих реквизитов, а для
|
||||
# базы вне хранилища они безвредны — поэтому подставляем всегда, когда они известны.
|
||||
repo = resolve_repository_settings(args)
|
||||
arguments.extend(repository_args(repo))
|
||||
|
||||
arguments += ["/LoadConfigFromFiles", f'"{args.ConfigDir}"']
|
||||
|
||||
if args.Mode == "Full":
|
||||
@@ -652,7 +765,7 @@ def main():
|
||||
arguments.extend(quote_if_needed(a) for a in extra_args)
|
||||
|
||||
# --- Execute ---
|
||||
print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
|
||||
print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName, repo['password'])}")
|
||||
result = run_v8(v8path, arguments)
|
||||
exit_code = result.returncode
|
||||
|
||||
@@ -685,6 +798,7 @@ def main():
|
||||
print("--- End ---")
|
||||
|
||||
print_platform_output(result)
|
||||
write_repository_hints(log_content)
|
||||
# Причину не называем: строки лога печатаются следом и говорят за себя, а класс проблемы
|
||||
# разный — от отброшенного свойства до нерабочей на этой платформе конфигурации. Подсказку
|
||||
# про -StrictLog не даём: загрузка уже выполнена, повторять её ради того же текста незачем.
|
||||
|
||||
@@ -76,7 +76,7 @@ allowed-tools:
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" <подкоманда> <параметры>
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" -Command <подкоманда> <параметры>
|
||||
```
|
||||
|
||||
### Рабочий цикл
|
||||
@@ -175,19 +175,19 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" <по
|
||||
|
||||
```powershell
|
||||
# Захватить справочник вместе с подчинёнными объектами
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" lock -InfoBasePath "C:\Bases\MyDB" -Objects "Справочник.Номенклатура" -WithChildren
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" -Command lock -InfoBasePath "C:\Bases\MyDB" -Objects "Справочник.Номенклатура" -WithChildren
|
||||
|
||||
# Захватить корень вместе с новым объектом
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" lock -InfoBasePath "C:\Bases\MyDB" -Objects "Конфигурация,Справочник.Склады"
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" -Command lock -InfoBasePath "C:\Bases\MyDB" -Objects "Конфигурация,Справочник.Склады"
|
||||
|
||||
# Поместить с комментарием, оставив захват
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" commit -InfoBasePath "C:\Bases\MyDB" -Objects "Справочник.Номенклатура" -Comment "Добавлен реквизит Артикул" -KeepLocked
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" -Command commit -InfoBasePath "C:\Bases\MyDB" -Objects "Справочник.Номенклатура" -Comment "Добавлен реквизит Артикул" -KeepLocked
|
||||
|
||||
# Получить изменения из хранилища
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" update -InfoBasePath "C:\Bases\MyDB"
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" -Command update -InfoBasePath "C:\Bases\MyDB"
|
||||
|
||||
# Серверная база, расширение
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" lock -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Extension "МоёРасширение" -Objects "Справочник.Номенклатура"
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" -Command lock -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Extension "МоёРасширение" -Objects "Справочник.Номенклатура"
|
||||
```
|
||||
|
||||
## После выполнения
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
## create — создать хранилище
|
||||
|
||||
```powershell
|
||||
... db-repo.ps1 create -InfoBasePath "C:\Bases\MyDB" -RepositoryPath "C:\Repo\MyApp" -RepositoryUser "Admin" -RepositoryPassword "…"
|
||||
... db-repo.ps1 -Command create -InfoBasePath "C:\Bases\MyDB" -RepositoryPath "C:\Repo\MyApp" -RepositoryUser "Admin" -RepositoryPassword "…"
|
||||
```
|
||||
|
||||
| Параметр | Описание |
|
||||
@@ -22,7 +22,7 @@
|
||||
## add-user — создать пользователя
|
||||
|
||||
```powershell
|
||||
... db-repo.ps1 add-user -InfoBasePath "C:\Bases\MyDB" -NewUser "Ivanov" -NewUserPassword "…" -Rights LockObjects
|
||||
... db-repo.ps1 -Command add-user -InfoBasePath "C:\Bases\MyDB" -NewUser "Ivanov" -NewUserPassword "…" -Rights LockObjects
|
||||
```
|
||||
|
||||
| Право | Что даёт |
|
||||
@@ -38,7 +38,7 @@
|
||||
## copy-users — скопировать пользователей из другого хранилища
|
||||
|
||||
```powershell
|
||||
... db-repo.ps1 copy-users -InfoBasePath "C:\Bases\MyDB" -SourcePath "\\srv01\repo\Other" -SourceUser "Admin" -SourcePassword "…"
|
||||
... db-repo.ps1 -Command copy-users -InfoBasePath "C:\Bases\MyDB" -SourcePath "\\srv01\repo\Other" -SourceUser "Admin" -SourcePassword "…"
|
||||
```
|
||||
|
||||
`-SourcePath`, `-SourceUser`, `-SourcePassword` описывают хранилище-**источник**. Удалённые пользователи
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
## connect — подключить
|
||||
|
||||
```powershell
|
||||
... db-repo.ps1 connect -InfoBasePath "C:\Bases\MyDB" -RepositoryPath "\\srv01\repo\MyApp" -RepositoryUser "Ivanov" -RepositoryPassword "…"
|
||||
... db-repo.ps1 -Command connect -InfoBasePath "C:\Bases\MyDB" -RepositoryPath "\\srv01\repo\MyApp" -RepositoryUser "Ivanov" -RepositoryPassword "…"
|
||||
```
|
||||
|
||||
| Параметр | Описание |
|
||||
@@ -19,7 +19,7 @@
|
||||
## disconnect — отключить
|
||||
|
||||
```powershell
|
||||
... db-repo.ps1 disconnect -InfoBasePath "C:\Bases\MyDB"
|
||||
... db-repo.ps1 -Command disconnect -InfoBasePath "C:\Bases\MyDB"
|
||||
```
|
||||
|
||||
**Спроси подтверждение у пользователя.** Операция необратима. Если пользователь аутентифицируется в хранилище, отключение отражается и
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
## report — отчёт по версиям
|
||||
|
||||
```powershell
|
||||
... db-repo.ps1 report -InfoBasePath "C:\Bases\MyDB" -OutputFile "C:\tmp\repo.txt"
|
||||
... db-repo.ps1 -Command report -InfoBasePath "C:\Bases\MyDB" -OutputFile "C:\tmp\repo.txt"
|
||||
```
|
||||
|
||||
| Параметр | Описание |
|
||||
@@ -35,7 +35,7 @@
|
||||
## dump-cfg — выгрузить версию в CF
|
||||
|
||||
```powershell
|
||||
... db-repo.ps1 dump-cfg -InfoBasePath "C:\Bases\MyDB" -OutputFile "C:\tmp\v120.cf" -Version 120
|
||||
... db-repo.ps1 -Command dump-cfg -InfoBasePath "C:\Bases\MyDB" -OutputFile "C:\tmp\v120.cf" -Version 120
|
||||
```
|
||||
|
||||
Без `-Version` (или при `-1`) выгружается последняя версия.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
## set-label — метка на версию
|
||||
|
||||
```powershell
|
||||
... db-repo.ps1 set-label -InfoBasePath "C:\Bases\MyDB" -Label "Релиз 1.2" -Version 120 -Comment "Передано в тест"
|
||||
... db-repo.ps1 -Command set-label -InfoBasePath "C:\Bases\MyDB" -Label "Релиз 1.2" -Version 120 -Comment "Передано в тест"
|
||||
```
|
||||
|
||||
Без `-Version` метка ставится на последнюю версию. Несуществующая версия — ошибка.
|
||||
@@ -11,7 +11,7 @@
|
||||
## optimize — оптимизация хранения
|
||||
|
||||
```powershell
|
||||
... db-repo.ps1 optimize -InfoBasePath "C:\Bases\MyDB"
|
||||
... db-repo.ps1 -Command optimize -InfoBasePath "C:\Bases\MyDB"
|
||||
```
|
||||
|
||||
Оптимизирует хранение данных в хранилище. Операция долгая.
|
||||
@@ -19,7 +19,7 @@
|
||||
## clear-cache — очистка кеша
|
||||
|
||||
```powershell
|
||||
... db-repo.ps1 clear-cache -InfoBasePath "C:\Bases\MyDB" -CacheScope local
|
||||
... db-repo.ps1 -Command clear-cache -InfoBasePath "C:\Bases\MyDB" -CacheScope local
|
||||
```
|
||||
|
||||
| `-CacheScope` | Что чистит |
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# db-repo v1.3 — 1C configuration repository operations
|
||||
# db-repo v1.4 — 1C configuration repository operations
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
# NB: движок только 1cv8 — ibcmd работу с хранилищем не поддерживает (нет такого режима).
|
||||
<#
|
||||
@@ -31,7 +31,9 @@
|
||||
|
||||
[CmdletBinding(PositionalBinding=$false)]
|
||||
param(
|
||||
[Parameter(Mandatory=$true, Position=0)]
|
||||
# Не Mandatory: обязательный параметр PowerShell запрашивает интерактивно, а в пакетном
|
||||
# запуске это зависание. Пустое значение проверяем сами.
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$Command,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
@@ -946,6 +948,10 @@ function Write-RepoVerdict {
|
||||
|
||||
# =============================== main ===============================
|
||||
|
||||
if (-not $Command) {
|
||||
Write-Host "Error: -Command is required. Known: $(($script:CommandKeys.Keys | Sort-Object) -join ', ')" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
$cmd = Resolve-Command $Command
|
||||
|
||||
if (-not $InfoBasePath -and -not ($InfoBaseServer -and $InfoBaseRef)) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -197,7 +197,7 @@ function Get-RepositoryArgs {
|
||||
$a += "/ConfigurationRepositoryF`"$($Repo.Path)`""
|
||||
if ($Repo.User) { $a += "/ConfigurationRepositoryN`"$($Repo.User)`"" }
|
||||
if ($Repo.Password) { $a += "/ConfigurationRepositoryP`"$($Repo.Password)`"" }
|
||||
return ,$a
|
||||
return $a
|
||||
}
|
||||
|
||||
function Protect-Secrets {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# db-update v1.16 — Update 1C database configuration
|
||||
# db-update v1.17 — Update 1C database configuration
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
@@ -72,10 +72,95 @@ IBCMD_OWNED_KEYS = [
|
||||
"--import", "--export", "--apply", "--force", "--create-database",
|
||||
"--user", "--password",
|
||||
]
|
||||
V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP"]
|
||||
V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP", "/ConfigurationRepositoryP"]
|
||||
IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"]
|
||||
|
||||
|
||||
# --- Реквизиты хранилища из .v8-project.json ---
|
||||
# Модель их не передаёт: скрипт сопоставляет параметры соединения с записью в databases[]
|
||||
# и берёт repository оттуда. Тот же приём, что в cf-edit.py (сопоставление по configSrc).
|
||||
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 same_path(a, b):
|
||||
if not a or not b:
|
||||
return False
|
||||
try:
|
||||
return os.path.abspath(a).rstrip("\\/").lower() == os.path.abspath(b).rstrip("\\/").lower()
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def find_project_database(args):
|
||||
"""Запись базы в реестре, соответствующая переданному соединению. None, если не найдена."""
|
||||
pf = _sg_find_v8project(os.getcwd())
|
||||
if not pf:
|
||||
return None
|
||||
try:
|
||||
with open(pf, encoding="utf-8-sig") as f:
|
||||
proj = json.load(f)
|
||||
except Exception:
|
||||
return None
|
||||
for db in proj.get("databases") or []:
|
||||
if args.InfoBasePath and db.get("path") and same_path(db["path"], args.InfoBasePath):
|
||||
return db
|
||||
if args.InfoBaseServer and args.InfoBaseRef and db.get("server") and db.get("ref"):
|
||||
if (db["server"].lower() == args.InfoBaseServer.lower()
|
||||
and db["ref"].lower() == args.InfoBaseRef.lower()):
|
||||
return db
|
||||
return None
|
||||
|
||||
|
||||
def resolve_repository_settings(args):
|
||||
"""Возвращает dict path/user/password/from_registry. Явные -Repository* сильнее реестра."""
|
||||
db_rec = find_project_database(args)
|
||||
rec = None
|
||||
if db_rec:
|
||||
if args.Extension:
|
||||
# У расширения СВОЁ хранилище со своим путём (проверено): выбирается парой
|
||||
# /ConfigurationRepositoryF"<путь расширения>" + -Extension "<Имя>".
|
||||
for ext in db_rec.get("extensions") or []:
|
||||
if (ext.get("name") or "").lower() == args.Extension.lower():
|
||||
rec = ext.get("repository")
|
||||
break
|
||||
else:
|
||||
rec = db_rec.get("repository")
|
||||
path = args.RepositoryPath or ((rec or {}).get("path") or None)
|
||||
user = args.RepositoryUser or ((rec or {}).get("user") or None)
|
||||
# Пустой пароль = отсутствующий: 1С требует опускать ключ целиком, а не передавать пустое значение.
|
||||
pwd = args.RepositoryPassword or ((rec or {}).get("password") or None)
|
||||
return {
|
||||
"path": path.strip().strip('"') if path else None,
|
||||
"user": user,
|
||||
"password": pwd,
|
||||
"from_registry": bool(rec and rec.get("path")),
|
||||
}
|
||||
|
||||
|
||||
def repository_args(repo):
|
||||
"""Ключи доступа к хранилищу. Форма — кавычки ВНУТРИ токена, как у /N и /P."""
|
||||
a = []
|
||||
if not repo or not repo.get("path"):
|
||||
return a
|
||||
a.append('/ConfigurationRepositoryF"%s"' % repo["path"])
|
||||
if repo.get("user"):
|
||||
a.append('/ConfigurationRepositoryN"%s"' % repo["user"])
|
||||
if repo.get("password"):
|
||||
a.append('/ConfigurationRepositoryP"%s"' % repo["password"])
|
||||
return a
|
||||
|
||||
|
||||
def arg_key_match(token, key):
|
||||
"""Token matches a key when it equals it, or starts with it and the next character
|
||||
is not a letter — catches glued /N"user" and --password=x, while keeping
|
||||
@@ -438,6 +523,9 @@ def main():
|
||||
parser.add_argument("-InfoBaseRef", default="")
|
||||
parser.add_argument("-UserName", default="")
|
||||
parser.add_argument("-Password", default="")
|
||||
parser.add_argument("-RepositoryPath", default="")
|
||||
parser.add_argument("-RepositoryUser", default="")
|
||||
parser.add_argument("-RepositoryPassword", default="")
|
||||
parser.add_argument("-Extension", default="")
|
||||
parser.add_argument("-AllExtensions", action="store_true")
|
||||
parser.add_argument("-Dynamic", default="", choices=["", "+", "-"])
|
||||
@@ -530,6 +618,11 @@ def main():
|
||||
if args.Password:
|
||||
arguments.append(f'/P"{args.Password}"')
|
||||
|
||||
# База под хранилищем не примет НИ ОДНОЙ операции конфигуратора без этих реквизитов, а для
|
||||
# базы вне хранилища они безвредны — поэтому подставляем всегда, когда они известны.
|
||||
repo = resolve_repository_settings(args)
|
||||
arguments.extend(repository_args(repo))
|
||||
|
||||
arguments.append("/UpdateDBCfg")
|
||||
|
||||
# --- Options ---
|
||||
@@ -553,7 +646,7 @@ def main():
|
||||
arguments.extend(quote_if_needed(a) for a in extra_args)
|
||||
|
||||
# --- Execute ---
|
||||
print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
|
||||
print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName, repo['password'])}")
|
||||
result = run_v8(v8path, arguments)
|
||||
exit_code = result.returncode
|
||||
|
||||
|
||||
Reference in New Issue
Block a user