mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-07-19 00:59:40 +03:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 13c08d8563 |
@@ -1,32 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "cc-1c-skills",
|
|
||||||
"interface": {
|
|
||||||
"displayName": "1C Skills"
|
|
||||||
},
|
|
||||||
"plugins": [
|
|
||||||
{
|
|
||||||
"name": "1c-skills",
|
|
||||||
"source": {
|
|
||||||
"source": "url",
|
|
||||||
"url": "https://github.com/Nikolay-Shirokov/cc-1c-skills.git",
|
|
||||||
"ref": "port-codex"
|
|
||||||
},
|
|
||||||
"policy": {
|
|
||||||
"installation": "AVAILABLE"
|
|
||||||
},
|
|
||||||
"category": "Development"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "1c-skills-py",
|
|
||||||
"source": {
|
|
||||||
"source": "url",
|
|
||||||
"url": "https://github.com/Nikolay-Shirokov/cc-1c-skills.git",
|
|
||||||
"ref": "port-codex-py"
|
|
||||||
},
|
|
||||||
"policy": {
|
|
||||||
"installation": "AVAILABLE"
|
|
||||||
},
|
|
||||||
"category": "Development"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://json.schemastore.org/claude-code-marketplace-manifest.json",
|
|
||||||
"name": "cc-1c-skills",
|
|
||||||
"description": "Маркетплейс навыков для разработки на платформе 1С:Предприятие",
|
|
||||||
"owner": {
|
|
||||||
"name": "Nikolay Shirokov"
|
|
||||||
},
|
|
||||||
"plugins": [
|
|
||||||
{
|
|
||||||
"name": "1c-skills",
|
|
||||||
"source": "./",
|
|
||||||
"description": "[PowerShell] Навыки для разработки на 1С:Предприятие 8.3 — абстракции над XML-форматами и CLI конфигуратора, плюс глаза и руки для тестирования через веб-клиент."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "1c-skills-py",
|
|
||||||
"source": {
|
|
||||||
"source": "github",
|
|
||||||
"repo": "Nikolay-Shirokov/cc-1c-skills",
|
|
||||||
"ref": "port-claude-code-py"
|
|
||||||
},
|
|
||||||
"description": "[Python] То же — для Linux/Mac или когда PowerShell недоступен."
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
|
|
||||||
"name": "1c-skills",
|
|
||||||
"description": "[PowerShell] Навыки для разработки на 1С:Предприятие 8.3 — абстракции над XML-форматами и CLI конфигуратора, плюс глаза и руки для тестирования через веб-клиент.",
|
|
||||||
"author": {
|
|
||||||
"name": "Nikolay Shirokov"
|
|
||||||
},
|
|
||||||
"homepage": "https://github.com/Nikolay-Shirokov/cc-1c-skills",
|
|
||||||
"repository": "https://github.com/Nikolay-Shirokov/cc-1c-skills",
|
|
||||||
"license": "MIT",
|
|
||||||
"keywords": [
|
|
||||||
"1c",
|
|
||||||
"1c-dev",
|
|
||||||
"cf",
|
|
||||||
"cfe",
|
|
||||||
"epf",
|
|
||||||
"erf",
|
|
||||||
"metadata",
|
|
||||||
"configuration",
|
|
||||||
"extension",
|
|
||||||
"form",
|
|
||||||
"report",
|
|
||||||
"skd",
|
|
||||||
"data-processor",
|
|
||||||
"mxl",
|
|
||||||
"web-client",
|
|
||||||
"testing",
|
|
||||||
"test-automation"
|
|
||||||
],
|
|
||||||
"skills": "./.claude/skills/"
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
---
|
|
||||||
name: mxl-decompile
|
|
||||||
description: Декомпиляция табличного документа (MXL) в JSON-определение. Используй когда нужно получить редактируемое описание существующего макета
|
|
||||||
argument-hint: <TemplatePath> [OutputPath]
|
|
||||||
allowed-tools:
|
|
||||||
- Bash
|
|
||||||
- Read
|
|
||||||
- Write
|
|
||||||
- Glob
|
|
||||||
---
|
|
||||||
|
|
||||||
# /mxl-decompile — Декомпилятор макета в DSL
|
|
||||||
|
|
||||||
Принимает Template.xml табличного документа 1С и генерирует компактное JSON-определение (DSL). Обратная операция к `/mxl-compile`.
|
|
||||||
|
|
||||||
## Использование
|
|
||||||
|
|
||||||
```
|
|
||||||
/mxl-decompile <TemplatePath> [OutputPath]
|
|
||||||
```
|
|
||||||
|
|
||||||
## Параметры
|
|
||||||
|
|
||||||
| Параметр | Обязательный | Описание |
|
|
||||||
|--------------|:------------:|-----------------------------------------|
|
|
||||||
| TemplatePath | да | Путь к Template.xml |
|
|
||||||
| OutputPath | нет | Путь для JSON (если не указан — stdout) |
|
|
||||||
|
|
||||||
## Команда
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/mxl-decompile.ps1" -TemplatePath "<путь>/Template.xml" [-OutputPath "<путь>.json"]
|
|
||||||
```
|
|
||||||
|
|
||||||
## Рабочий процесс
|
|
||||||
|
|
||||||
Декомпиляция существующего макета для анализа или доработки:
|
|
||||||
|
|
||||||
1. Claude вызывает `/mxl-decompile` для получения JSON из Template.xml
|
|
||||||
2. Claude анализирует или модифицирует JSON (добавляет области, меняет стили)
|
|
||||||
3. Claude вызывает `/mxl-compile` для генерации нового Template.xml
|
|
||||||
4. Claude вызывает `/mxl-validate` для проверки
|
|
||||||
|
|
||||||
## JSON-схема DSL
|
|
||||||
|
|
||||||
Полная спецификация формата: **`docs/mxl-dsl-spec.md`** (прочитать через Read tool).
|
|
||||||
|
|
||||||
## Генерация имён
|
|
||||||
|
|
||||||
Скрипт автоматически генерирует осмысленные имена:
|
|
||||||
|
|
||||||
- **Шрифты**: `default`, `bold`, `header`, `small`, `italic` — или описательные имена по свойствам
|
|
||||||
- **Стили**: `bordered`, `bordered-center`, `bold-right`, `border-top` и т.д. — по комбинации свойств
|
|
||||||
|
|
||||||
## Детектирование `rowStyle`
|
|
||||||
|
|
||||||
Если в строке есть пустые ячейки (без параметров/текста) и все они имеют одинаковый формат — этот формат распознаётся как `rowStyle`, а пустые ячейки исключаются из вывода.
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
// web-test cli/commands/status v1.0 — check session
|
|
||||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
import { existsSync, readFileSync } from 'fs';
|
|
||||||
import { out } from '../util.mjs';
|
|
||||||
import { SESSION_FILE } from '../session.mjs';
|
|
||||||
|
|
||||||
export function cmdStatus() {
|
|
||||||
if (!existsSync(SESSION_FILE)) {
|
|
||||||
out({ ok: false, message: 'No active session' });
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
const sess = JSON.parse(readFileSync(SESSION_FILE, 'utf-8'));
|
|
||||||
out({ ok: true, ...sess });
|
|
||||||
}
|
|
||||||
@@ -1,458 +0,0 @@
|
|||||||
// web-test cli/commands/test v1.3 — regression test runner
|
|
||||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
import { existsSync, writeFileSync, mkdirSync } from 'fs';
|
|
||||||
import { resolve, dirname, basename, relative } from 'path';
|
|
||||||
import * as browser from '../../browser.mjs';
|
|
||||||
import { out, die, elapsed, slugify, formatDuration, interpolate, printSteps } from '../util.mjs';
|
|
||||||
import { buildContext, buildScopedContext } from '../exec-context.mjs';
|
|
||||||
import { createAssertions } from '../test-runner/assertions.mjs';
|
|
||||||
import { buildSeverityIndex } from '../test-runner/severity.mjs';
|
|
||||||
import { writeAllure, buildJUnit, syncAllureExtras } from '../test-runner/reporters.mjs';
|
|
||||||
import { discoverTests, resetState } from '../test-runner/discover.mjs';
|
|
||||||
|
|
||||||
export async function cmdTest(rawArgs) {
|
|
||||||
// Split off everything after `--` — those args belong to user-defined hooks
|
|
||||||
// (see spec §6: "all arguments after `--` are forwarded verbatim to _hooks.mjs
|
|
||||||
// via the hookArgs field; the runner does not interpret them").
|
|
||||||
const sepIdx = rawArgs.indexOf('--');
|
|
||||||
const ownArgs = sepIdx >= 0 ? rawArgs.slice(0, sepIdx) : rawArgs;
|
|
||||||
const hookArgs = sepIdx >= 0 ? rawArgs.slice(sepIdx + 1) : [];
|
|
||||||
|
|
||||||
// Parse flags
|
|
||||||
const opts = { bail: false, retry: 0, timeout: 30000, report: null, format: 'json', screenshot: null, reportDir: null, record: false };
|
|
||||||
let tags = null, grep = null, urlFlag = null;
|
|
||||||
const positional = [];
|
|
||||||
for (const a of ownArgs) {
|
|
||||||
if (a.startsWith('--tags=')) tags = a.slice(7).split(',');
|
|
||||||
else if (a.startsWith('--grep=')) grep = new RegExp(a.slice(7), 'i');
|
|
||||||
else if (a.startsWith('--url=')) urlFlag = a.slice(6);
|
|
||||||
else if (a === '--bail') opts.bail = true;
|
|
||||||
else if (a.startsWith('--retry=')) opts.retry = parseInt(a.slice(8)) || 0;
|
|
||||||
else if (a.startsWith('--timeout=')) opts.timeout = parseInt(a.slice(10)) || 30000;
|
|
||||||
else if (a.startsWith('--report=')) opts.report = a.slice(9);
|
|
||||||
else if (a.startsWith('--format=')) opts.format = a.slice(9);
|
|
||||||
else if (a.startsWith('--screenshot=')) opts.screenshot = a.slice(13);
|
|
||||||
else if (a.startsWith('--report-dir=')) opts.reportDir = a.slice(13);
|
|
||||||
else if (a === '--record') opts.record = true;
|
|
||||||
else if (!a.startsWith('--')) positional.push(a);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Positional args are ALWAYS test paths (one or many). URL comes from --url= or config
|
|
||||||
// (see webtest.config.mjs). This matches pytest/jest/playwright; a positional that looks
|
|
||||||
// like a URL is a mistake → fail fast with a hint instead of feeding it to page.goto().
|
|
||||||
const isUrl = (s) => /^https?:\/\//i.test(s);
|
|
||||||
let url = urlFlag || null;
|
|
||||||
const testPaths = [...positional];
|
|
||||||
if (testPaths.length === 0) {
|
|
||||||
die('Usage: node run.mjs test <dir|file>... [--url=URL] [--tags=...] [--grep=...] [--bail] [--retry=N] [--timeout=ms] [--report=path]');
|
|
||||||
}
|
|
||||||
for (const p of testPaths) {
|
|
||||||
if (existsSync(resolve(p))) continue;
|
|
||||||
if (isUrl(p)) {
|
|
||||||
die(`"${p}" looks like a URL — use --url=<url>; positional args are test paths.`);
|
|
||||||
}
|
|
||||||
die(`Test path not found: "${p}". To run a subset use --grep= / --tags=, or pass an existing dir/file.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load config if exists. config (webtest.config.mjs) and hooks (_hooks.mjs) resolve from
|
|
||||||
// the FIRST path's directory — list paths from the same suite folder.
|
|
||||||
const firstPath = resolve(testPaths[0]);
|
|
||||||
const isFile = firstPath.endsWith('.test.mjs');
|
|
||||||
const testDir = isFile ? dirname(firstPath) : firstPath;
|
|
||||||
const configPath = resolve(testDir, 'webtest.config.mjs');
|
|
||||||
let config = {};
|
|
||||||
if (existsSync(configPath)) {
|
|
||||||
const mod = await import('file:///' + configPath.replace(/\\/g, '/'));
|
|
||||||
config = mod.default || {};
|
|
||||||
}
|
|
||||||
const severityIndex = buildSeverityIndex(config);
|
|
||||||
|
|
||||||
// Build context registry: name → url. Supports config.contexts or single config.url / CLI url.
|
|
||||||
const contextSpecs = {};
|
|
||||||
let defaultContextName = 'default';
|
|
||||||
const defaultIsolation = config.isolation || 'tab';
|
|
||||||
if (config.contexts && typeof config.contexts === 'object' && Object.keys(config.contexts).length) {
|
|
||||||
for (const [n, spec] of Object.entries(config.contexts)) {
|
|
||||||
contextSpecs[n] = { ...spec };
|
|
||||||
}
|
|
||||||
defaultContextName = config.defaultContext || Object.keys(config.contexts)[0];
|
|
||||||
if (url) contextSpecs[defaultContextName] = { ...contextSpecs[defaultContextName], url };
|
|
||||||
} else {
|
|
||||||
const fallbackUrl = url || config.url;
|
|
||||||
if (!fallbackUrl) die('No URL provided and no webtest.config.mjs found');
|
|
||||||
contextSpecs.default = { url: fallbackUrl };
|
|
||||||
}
|
|
||||||
if (!contextSpecs[defaultContextName]) {
|
|
||||||
die(`defaultContext "${defaultContextName}" not found in contexts: [${Object.keys(contextSpecs).join(', ')}]`);
|
|
||||||
}
|
|
||||||
if (!url) url = contextSpecs[defaultContextName].url;
|
|
||||||
|
|
||||||
// Apply config defaults (CLI flags override)
|
|
||||||
if (!tags && config.tags) tags = config.tags;
|
|
||||||
opts.timeout = ownArgs.some(a => a.startsWith('--timeout=')) ? opts.timeout : (config.timeout || opts.timeout);
|
|
||||||
opts.retry = ownArgs.some(a => a.startsWith('--retry=')) ? opts.retry : (config.retries || opts.retry);
|
|
||||||
if (config.preserveClipboard === false && !ownArgs.includes('--no-preserve-clipboard')) {
|
|
||||||
browser.setPreserveClipboard(false);
|
|
||||||
}
|
|
||||||
opts.record = opts.record || !!config.record;
|
|
||||||
opts.screenshot = opts.screenshot || config.screenshot || 'on-failure';
|
|
||||||
if (!['on-failure', 'every-step', 'off'].includes(opts.screenshot)) {
|
|
||||||
die(`Invalid --screenshot=${opts.screenshot} (expected on-failure|every-step|off)`);
|
|
||||||
}
|
|
||||||
if (!['json', 'allure', 'junit'].includes(opts.format)) {
|
|
||||||
die(`Invalid --format=${opts.format} (expected json|allure|junit)`);
|
|
||||||
}
|
|
||||||
if (opts.format === 'junit' && !opts.report) {
|
|
||||||
die('--format=junit requires --report=path.xml');
|
|
||||||
}
|
|
||||||
// `--report=-` means "machine report to stdout" (Unix `-` convention).
|
|
||||||
// Only meaningful for streamable formats (json/junit); allure is a directory.
|
|
||||||
const reportToStdout = opts.report === '-';
|
|
||||||
if (reportToStdout && opts.format === 'allure') {
|
|
||||||
die('--report=- (stdout) is not valid with --format=allure: allure emits a directory of files, not a single stream. Use --report-dir=<dir> instead.');
|
|
||||||
}
|
|
||||||
const reportDir = opts.reportDir
|
|
||||||
? resolve(opts.reportDir)
|
|
||||||
: (opts.report && !reportToStdout ? dirname(resolve(opts.report)) : testDir);
|
|
||||||
if (opts.screenshot !== 'off') {
|
|
||||||
try { mkdirSync(reportDir, { recursive: true }); } catch {}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Discover test files
|
|
||||||
const testFiles = discoverTests(testPaths);
|
|
||||||
if (!testFiles.length) die(`No *.test.mjs files found in ${testPaths.join(', ')}`);
|
|
||||||
|
|
||||||
// Import and filter tests
|
|
||||||
const tests = [];
|
|
||||||
let hasOnly = false;
|
|
||||||
for (const file of testFiles) {
|
|
||||||
const mod = await import('file:///' + file.replace(/\\/g, '/'));
|
|
||||||
const base = {
|
|
||||||
file: relative(testDir, file).replace(/\\/g, '/'),
|
|
||||||
name: mod.name || basename(file, '.test.mjs'),
|
|
||||||
tags: mod.tags || [],
|
|
||||||
timeout: mod.timeout || opts.timeout,
|
|
||||||
skip: mod.skip || false,
|
|
||||||
only: mod.only || false,
|
|
||||||
setup: mod.setup,
|
|
||||||
teardown: mod.teardown,
|
|
||||||
fn: mod.default,
|
|
||||||
param: undefined,
|
|
||||||
context: mod.context || null,
|
|
||||||
contexts: Array.isArray(mod.contexts) ? mod.contexts : null,
|
|
||||||
severity: typeof mod.severity === 'string' ? mod.severity : null,
|
|
||||||
};
|
|
||||||
if (base.only) hasOnly = true;
|
|
||||||
if (Array.isArray(mod.params) && mod.params.length) {
|
|
||||||
for (let i = 0; i < mod.params.length; i++) {
|
|
||||||
const p = mod.params[i];
|
|
||||||
const name = base.name.includes('{') ? interpolate(base.name, p) : `${base.name}[${i}]`;
|
|
||||||
tests.push({ ...base, name, param: p });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
tests.push(base);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter
|
|
||||||
const filtered = tests.filter(t => {
|
|
||||||
if (hasOnly && !t.only) return false;
|
|
||||||
if (tags && !tags.some(tag => t.tags.includes(tag))) return false;
|
|
||||||
if (grep && !grep.test(t.name)) return false;
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Load hooks
|
|
||||||
const hooksPath = resolve(testDir, '_hooks.mjs');
|
|
||||||
let hooks = {};
|
|
||||||
if (existsSync(hooksPath)) {
|
|
||||||
hooks = await import('file:///' + hooksPath.replace(/\\/g, '/'));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Human-readable report goes to stdout (test-runner convention: jest/pytest/playwright).
|
|
||||||
// In `--report -` mode the machine JSON/XML takes over stdout, so progress moves to stderr.
|
|
||||||
const W = reportToStdout ? process.stderr : process.stdout;
|
|
||||||
W.write(`\nweb-test -- ${url}\n`);
|
|
||||||
W.write(`Running ${filtered.length} tests from ${relative(process.cwd(), testDir).replace(/\\/g, '/') || '.'}/\n\n`);
|
|
||||||
|
|
||||||
const startedAt = new Date().toISOString();
|
|
||||||
const results = [];
|
|
||||||
let passCount = 0, failCount = 0, skipCount = 0;
|
|
||||||
|
|
||||||
const hookLog = (...a) => W.write(`[hooks] ${a.map(String).join(' ')}\n`);
|
|
||||||
const hookEnv = { hookArgs, log: hookLog, config };
|
|
||||||
if (hooks.prepare) await hooks.prepare(hookEnv);
|
|
||||||
|
|
||||||
// Lazy context creation
|
|
||||||
async function ensureContext(name) {
|
|
||||||
if (browser.hasContext(name)) return;
|
|
||||||
const spec = contextSpecs[name];
|
|
||||||
if (!spec) throw new Error(`Unknown context "${name}". Defined: [${Object.keys(contextSpecs).join(', ')}]`);
|
|
||||||
await browser.createContext(name, spec.url, { isolation: spec.isolation || defaultIsolation });
|
|
||||||
if (hooks.afterOpenContext && hookCtx) {
|
|
||||||
try { await hooks.afterOpenContext(hookCtx, name, spec); }
|
|
||||||
catch (e) { hookLog(`afterOpenContext("${name}") threw: ${e.message.split('\n')[0]}`); }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let hookCtx = null;
|
|
||||||
|
|
||||||
function wrapCloseContextHook(target) {
|
|
||||||
const orig = target.closeContext;
|
|
||||||
if (typeof orig !== 'function') return;
|
|
||||||
target.closeContext = async (name) => {
|
|
||||||
if (hooks.beforeCloseContext) {
|
|
||||||
try { await hooks.beforeCloseContext(target, name, contextSpecs[name]); }
|
|
||||||
catch (e) { hookLog(`beforeCloseContext("${name}") threw: ${e.message.split('\n')[0]}`); }
|
|
||||||
}
|
|
||||||
return await orig(name);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Connect: create default context up front
|
|
||||||
await ensureContext(defaultContextName);
|
|
||||||
|
|
||||||
const ctx = buildContext({ noRecord: false });
|
|
||||||
ctx.assert = createAssertions();
|
|
||||||
ctx.log = (...a) => { /* per-test, overridden below */ };
|
|
||||||
wrapCloseContextHook(ctx);
|
|
||||||
hookCtx = ctx;
|
|
||||||
|
|
||||||
// Default context was created BEFORE hookCtx existed → fire afterOpenContext now.
|
|
||||||
if (hooks.afterOpenContext) {
|
|
||||||
try { await hooks.afterOpenContext(ctx, defaultContextName, contextSpecs[defaultContextName]); }
|
|
||||||
catch (e) { hookLog(`afterOpenContext("${defaultContextName}") threw: ${e.message.split('\n')[0]}`); }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hooks.beforeAll) await hooks.beforeAll(ctx);
|
|
||||||
|
|
||||||
let testIdx = 0;
|
|
||||||
for (const t of filtered) {
|
|
||||||
testIdx++;
|
|
||||||
const declaredContexts = t.contexts && t.contexts.length
|
|
||||||
? t.contexts
|
|
||||||
: [t.context || defaultContextName];
|
|
||||||
|
|
||||||
if (t.skip) {
|
|
||||||
const reason = typeof t.skip === 'string' ? t.skip : '';
|
|
||||||
W.write(` ○ ${t.name}${reason ? ` (skip: ${reason})` : ' (skip)'}\n`);
|
|
||||||
results.push({ name: t.name, file: t.file, tags: t.tags, contexts: declaredContexts, status: 'skipped', duration: 0, attempts: 0, steps: [], output: '', error: null, screenshot: null });
|
|
||||||
skipCount++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const testContextNames = declaredContexts;
|
|
||||||
try {
|
|
||||||
for (const cn of testContextNames) await ensureContext(cn);
|
|
||||||
await browser.setActiveContext(testContextNames[0]);
|
|
||||||
} catch (e) {
|
|
||||||
W.write(` ✗ ${t.name} (context setup failed: ${e.message})\n`);
|
|
||||||
results.push({ name: t.name, file: t.file, tags: t.tags, contexts: declaredContexts, status: 'failed', duration: 0, attempts: 0, steps: [], output: '', error: { message: e.message }, screenshot: null });
|
|
||||||
failCount++;
|
|
||||||
if (opts.bail) break;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let lastError = null;
|
|
||||||
let testResult = null;
|
|
||||||
const maxAttempts = 1 + opts.retry;
|
|
||||||
|
|
||||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
||||||
const output = [];
|
|
||||||
let steps = [];
|
|
||||||
let currentSteps = steps;
|
|
||||||
let stepIdx = 0;
|
|
||||||
const t0 = Date.now();
|
|
||||||
|
|
||||||
ctx.testInfo = {
|
|
||||||
name: t.name,
|
|
||||||
file: basename(t.file),
|
|
||||||
filePath: t.file,
|
|
||||||
tags: t.tags,
|
|
||||||
timeout: t.timeout,
|
|
||||||
attempt,
|
|
||||||
maxAttempts,
|
|
||||||
param: t.param,
|
|
||||||
contexts: Object.fromEntries(testContextNames.map(n => [n, contextSpecs[n]])),
|
|
||||||
primaryContext: testContextNames[0],
|
|
||||||
};
|
|
||||||
ctx.testResult = null;
|
|
||||||
|
|
||||||
let videoFile = null;
|
|
||||||
if (opts.record) {
|
|
||||||
videoFile = resolve(reportDir, `${testIdx}-${slugify(t.name)}.mp4`);
|
|
||||||
try { await browser.startRecording(videoFile, { force: true }); } catch { videoFile = null; }
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.log = (...a) => output.push(a.map(String).join(' '));
|
|
||||||
ctx.step = async (name, fn) => {
|
|
||||||
const s = { name, start: Date.now(), status: 'passed', steps: [] };
|
|
||||||
currentSteps.push(s);
|
|
||||||
const prev = currentSteps;
|
|
||||||
currentSteps = s.steps;
|
|
||||||
stepIdx++;
|
|
||||||
const myIdx = stepIdx;
|
|
||||||
try {
|
|
||||||
await fn();
|
|
||||||
} catch (e) {
|
|
||||||
s.status = 'failed';
|
|
||||||
s.error = e.message;
|
|
||||||
throw e;
|
|
||||||
} finally {
|
|
||||||
s.stop = Date.now();
|
|
||||||
currentSteps = prev;
|
|
||||||
if (opts.screenshot === 'every-step' && s.status === 'passed') {
|
|
||||||
try {
|
|
||||||
const slug = slugify(name);
|
|
||||||
const file = resolve(reportDir, `${testIdx}-${myIdx}-${slug}.png`);
|
|
||||||
const png = await browser.screenshot();
|
|
||||||
writeFileSync(file, png);
|
|
||||||
s.screenshot = file;
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const scopedKeys = [];
|
|
||||||
if (t.contexts && t.contexts.length) {
|
|
||||||
for (const cn of t.contexts) {
|
|
||||||
ctx[cn] = buildScopedContext(cn);
|
|
||||||
wrapCloseContextHook(ctx[cn]);
|
|
||||||
scopedKeys.push(cn);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (hooks.beforeEach) await hooks.beforeEach(ctx);
|
|
||||||
if (t.setup) await t.setup(ctx);
|
|
||||||
|
|
||||||
let timeoutTimer;
|
|
||||||
try {
|
|
||||||
await Promise.race([
|
|
||||||
t.fn(ctx, t.param),
|
|
||||||
new Promise((_, reject) => { timeoutTimer = setTimeout(() => reject(new Error(`Timeout (${t.timeout}ms)`)), t.timeout); }),
|
|
||||||
]);
|
|
||||||
} finally {
|
|
||||||
// Clear the guard timer — otherwise it stays armed in the event loop and,
|
|
||||||
// since the success path never calls process.exit(), node can't exit until
|
|
||||||
// it fires (up to `timeout` ms after the last test finished).
|
|
||||||
clearTimeout(timeoutTimer);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (t.teardown) try { await t.teardown(ctx); } catch {}
|
|
||||||
ctx.testResult = { status: 'passed', duration: elapsed(t0), attempts: attempt, error: null, steps };
|
|
||||||
if (hooks.afterEach) try { await hooks.afterEach(ctx); } catch {}
|
|
||||||
for (const cn of testContextNames) {
|
|
||||||
try { await browser.setActiveContext(cn); await resetState(ctx); } catch {}
|
|
||||||
}
|
|
||||||
for (const k of scopedKeys) delete ctx[k];
|
|
||||||
|
|
||||||
if (videoFile) {
|
|
||||||
try { await browser.stopRecording(); } catch {}
|
|
||||||
}
|
|
||||||
const dur = elapsed(t0);
|
|
||||||
testResult = { name: t.name, file: t.file, tags: t.tags, contexts: testContextNames, severity: t.severity, status: 'passed', duration: dur, attempts: attempt, start: t0, stop: Date.now(), steps, output: output.join('\n'), error: null, screenshot: null, video: videoFile };
|
|
||||||
lastError = null;
|
|
||||||
break;
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
// Screenshot on failure FIRST — before teardown/afterEach/resetState reset the UI.
|
|
||||||
let shotFile = e.onecError?.screenshot;
|
|
||||||
if (!shotFile && opts.screenshot !== 'off') {
|
|
||||||
try {
|
|
||||||
const png = await browser.screenshot();
|
|
||||||
shotFile = resolve(reportDir, `error-${testIdx}-${slugify(t.file.replace(/\.test\.mjs$/, ''))}.png`);
|
|
||||||
writeFileSync(shotFile, png);
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (t.teardown) try { await t.teardown(ctx); } catch {}
|
|
||||||
const errInfo = { message: e.message, step: e.onecError?.step, screenshot: shotFile, onecError: e.onecError };
|
|
||||||
ctx.testResult = { status: 'failed', duration: elapsed(t0), attempts: attempt, error: errInfo, steps };
|
|
||||||
if (hooks.afterEach) try { await hooks.afterEach(ctx); } catch {}
|
|
||||||
for (const cn of testContextNames) {
|
|
||||||
try { await browser.setActiveContext(cn); await resetState(ctx); } catch {}
|
|
||||||
}
|
|
||||||
for (const k of scopedKeys) delete ctx[k];
|
|
||||||
|
|
||||||
if (videoFile) {
|
|
||||||
try { await browser.stopRecording(); } catch {}
|
|
||||||
}
|
|
||||||
lastError = errInfo;
|
|
||||||
const dur = elapsed(t0);
|
|
||||||
testResult = { name: t.name, file: t.file, tags: t.tags, contexts: testContextNames, severity: t.severity, status: 'failed', duration: dur, attempts: attempt, start: t0, stop: Date.now(), steps, output: output.join('\n'), error: errInfo, screenshot: shotFile, video: videoFile };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
results.push(testResult);
|
|
||||||
|
|
||||||
if (testResult.status === 'passed') {
|
|
||||||
passCount++;
|
|
||||||
W.write(` ✓ ${t.name} (${testResult.duration}s)\n`);
|
|
||||||
} else {
|
|
||||||
failCount++;
|
|
||||||
W.write(` ✗ ${t.name} (${testResult.duration}s)\n`);
|
|
||||||
printSteps(W, testResult.steps, ' ');
|
|
||||||
if (lastError?.message) W.write(` ${lastError.message}\n`);
|
|
||||||
if (lastError?.screenshot) W.write(` screenshot: ${lastError.screenshot}\n`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (opts.bail && testResult.status === 'failed') break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hooks.afterAll) try { await hooks.afterAll(ctx); } catch {}
|
|
||||||
|
|
||||||
} finally {
|
|
||||||
// Per-context teardown
|
|
||||||
try {
|
|
||||||
const remaining = browser.listContexts();
|
|
||||||
if (remaining.length > 0) {
|
|
||||||
const survivor = remaining[0];
|
|
||||||
try { await browser.setActiveContext(survivor); } catch {}
|
|
||||||
for (let i = remaining.length - 1; i >= 1; i--) {
|
|
||||||
const name = remaining[i];
|
|
||||||
if (hooks.beforeCloseContext && hookCtx) {
|
|
||||||
try { await hooks.beforeCloseContext(hookCtx, name, contextSpecs[name]); }
|
|
||||||
catch (e) { hookLog(`beforeCloseContext("${name}") threw: ${e.message.split('\n')[0]}`); }
|
|
||||||
}
|
|
||||||
try { await browser.closeContext(name); }
|
|
||||||
catch (e) { hookLog(`closeContext("${name}") failed: ${e.message.split('\n')[0]}`); }
|
|
||||||
}
|
|
||||||
if (hooks.beforeCloseContext && hookCtx) {
|
|
||||||
try { await hooks.beforeCloseContext(hookCtx, survivor, contextSpecs[survivor]); }
|
|
||||||
catch (e) { hookLog(`beforeCloseContext("${survivor}") threw: ${e.message.split('\n')[0]}`); }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
hookLog(`final teardown loop failed: ${e.message.split('\n')[0]}`);
|
|
||||||
}
|
|
||||||
try { await browser.disconnect(); } catch {}
|
|
||||||
if (hooks.cleanup) try { await hooks.cleanup(hookEnv); } catch {}
|
|
||||||
}
|
|
||||||
|
|
||||||
const finishedAt = new Date().toISOString();
|
|
||||||
const totalDuration = results.reduce((s, r) => s + r.duration, 0);
|
|
||||||
|
|
||||||
W.write(`\n${passCount} passed, ${failCount} failed, ${skipCount} skipped (${formatDuration(totalDuration)})\n\n`);
|
|
||||||
|
|
||||||
const report = {
|
|
||||||
runner: 'web-test', url, startedAt, finishedAt,
|
|
||||||
duration: totalDuration,
|
|
||||||
summary: { total: results.length, passed: passCount, failed: failCount, skipped: skipCount },
|
|
||||||
tests: results,
|
|
||||||
};
|
|
||||||
if (opts.format === 'allure') {
|
|
||||||
writeAllure(results, reportDir, severityIndex);
|
|
||||||
syncAllureExtras(testDir, reportDir);
|
|
||||||
} else if (opts.format === 'junit') {
|
|
||||||
if (reportToStdout) process.stdout.write(buildJUnit(report, testDir) + '\n');
|
|
||||||
else writeFileSync(resolve(opts.report), buildJUnit(report, testDir));
|
|
||||||
} else if (reportToStdout) {
|
|
||||||
out(report);
|
|
||||||
} else if (opts.report) {
|
|
||||||
writeFileSync(resolve(opts.report), JSON.stringify(report, null, 2));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (failCount > 0) process.exit(1);
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
// web-test cli/test-runner/discover v1.1 — test file discovery + state reset between tests
|
|
||||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
import { existsSync, readdirSync } from 'fs';
|
|
||||||
import { resolve } from 'path';
|
|
||||||
|
|
||||||
// Accepts a single path or an array of paths (files and/or dirs). Each .test.mjs file is
|
|
||||||
// taken directly; each directory is walked recursively (skipping _ / . prefixes). Results
|
|
||||||
// are deduped and sorted — sorting preserves the numeric-prefix order the suite relies on
|
|
||||||
// (00-, 01-, …) even when paths are listed out of order.
|
|
||||||
export function discoverTests(testPaths) {
|
|
||||||
const paths = Array.isArray(testPaths) ? testPaths : [testPaths];
|
|
||||||
const files = [];
|
|
||||||
function walk(dir) {
|
|
||||||
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
||||||
if (entry.name.startsWith('_') || entry.name.startsWith('.')) continue;
|
|
||||||
const full = resolve(dir, entry.name);
|
|
||||||
if (entry.isDirectory()) walk(full);
|
|
||||||
else if (entry.name.endsWith('.test.mjs')) files.push(full);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const p of paths) {
|
|
||||||
const full = resolve(p);
|
|
||||||
if (full.endsWith('.test.mjs')) {
|
|
||||||
if (existsSync(full)) files.push(full);
|
|
||||||
} else if (existsSync(full)) {
|
|
||||||
walk(full);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return [...new Set(files)].sort();
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function resetState(ctx) {
|
|
||||||
try { if (typeof ctx.dismissPendingErrors === 'function') await ctx.dismissPendingErrors(); } catch {}
|
|
||||||
for (let i = 0; i < 10; i++) {
|
|
||||||
try {
|
|
||||||
const state = await ctx.getFormState();
|
|
||||||
// form === null means no form open (desktop). form === 0 is a real background form
|
|
||||||
// 1C exposes in some states — must still close it to fully reset.
|
|
||||||
if (state.form == null) break;
|
|
||||||
await ctx.closeForm({ save: false });
|
|
||||||
} catch { break; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,404 +0,0 @@
|
|||||||
// web-test core/session v1.17 — Browser session lifecycle: connect/disconnect/attach/detach, multi-context registry.
|
|
||||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
|
|
||||||
import { chromium } from 'playwright';
|
|
||||||
import { statSync, mkdirSync, readdirSync, rmSync } from 'fs';
|
|
||||||
import { join as pathJoin } from 'path';
|
|
||||||
import { tmpdir } from 'os';
|
|
||||||
import {
|
|
||||||
browser, page, sessionPrefix, seanceId, recorder, highlightMode,
|
|
||||||
contexts, activeContextName, activeMode, persistentUserDataDir,
|
|
||||||
setBrowser, setPage, setSessionPrefix, setSeanceId, setHighlightMode,
|
|
||||||
setActiveContextName, setActiveMode, setPersistentUserDataDir,
|
|
||||||
isConnected, LOAD_TIMEOUT, INIT_TIMEOUT, EXT_ID,
|
|
||||||
} from './state.mjs';
|
|
||||||
import { closeModals } from './errors.mjs';
|
|
||||||
import { stopRecording } from '../recording/capture.mjs';
|
|
||||||
import { getPageState } from '../nav/navigation.mjs';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find the 1C browser extension in Chrome/Edge user profiles.
|
|
||||||
* Returns the path to the latest version, or null if not found.
|
|
||||||
* Can be overridden via extensionPath in .v8-project.json.
|
|
||||||
*/
|
|
||||||
function findExtension(overridePath) {
|
|
||||||
if (overridePath) {
|
|
||||||
try { if (statSync(overridePath).isDirectory()) return overridePath; } catch {}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const localAppData = process.env.LOCALAPPDATA;
|
|
||||||
if (!localAppData) return null;
|
|
||||||
const browsers = [
|
|
||||||
pathJoin(localAppData, 'Google', 'Chrome', 'User Data'),
|
|
||||||
pathJoin(localAppData, 'Microsoft', 'Edge', 'User Data'),
|
|
||||||
];
|
|
||||||
for (const userData of browsers) {
|
|
||||||
try { if (!statSync(userData).isDirectory()) continue; } catch { continue; }
|
|
||||||
let profiles;
|
|
||||||
try { profiles = readdirSync(userData).filter(d => d === 'Default' || d.startsWith('Profile ')); } catch { continue; }
|
|
||||||
for (const profile of profiles) {
|
|
||||||
const extDir = pathJoin(userData, profile, 'Extensions', EXT_ID);
|
|
||||||
try { if (!statSync(extDir).isDirectory()) continue; } catch { continue; }
|
|
||||||
let versions;
|
|
||||||
try { versions = readdirSync(extDir).filter(d => /^\d/.test(d)).sort(); } catch { continue; }
|
|
||||||
if (versions.length > 0) {
|
|
||||||
const best = pathJoin(extDir, versions[versions.length - 1]);
|
|
||||||
try { if (statSync(pathJoin(best, 'manifest.json')).isFile()) return best; } catch {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* isConnected moved to core/state.mjs */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Open browser and navigate to 1C web client URL.
|
|
||||||
* Waits for initialization (themesCell_theme_0 selector) and attempts to close startup modals.
|
|
||||||
*/
|
|
||||||
export async function connect(url, { extensionPath } = {}) {
|
|
||||||
if (isConnected()) {
|
|
||||||
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: LOAD_TIMEOUT });
|
|
||||||
} else {
|
|
||||||
const extPath = findExtension(extensionPath);
|
|
||||||
if (extPath) {
|
|
||||||
// Launch with 1C browser extension via persistent context
|
|
||||||
setPersistentUserDataDir(pathJoin(tmpdir(), 'pw-1c-ext-' + Date.now()));
|
|
||||||
mkdirSync(persistentUserDataDir, { recursive: true });
|
|
||||||
const context = await chromium.launchPersistentContext(persistentUserDataDir, {
|
|
||||||
headless: false,
|
|
||||||
args: [
|
|
||||||
'--start-maximized',
|
|
||||||
'--disable-extensions-except=' + extPath,
|
|
||||||
'--load-extension=' + extPath,
|
|
||||||
],
|
|
||||||
viewport: null,
|
|
||||||
permissions: ['clipboard-read', 'clipboard-write'],
|
|
||||||
});
|
|
||||||
setBrowser(context); // persistent context IS the browser
|
|
||||||
setPage(context.pages()[0] || await context.newPage());
|
|
||||||
} else {
|
|
||||||
// Fallback: launch without extension
|
|
||||||
setBrowser(await chromium.launch({ headless: false, args: ['--start-maximized'] }));
|
|
||||||
const context = await browser.newContext({
|
|
||||||
viewport: null,
|
|
||||||
permissions: ['clipboard-read', 'clipboard-write'],
|
|
||||||
});
|
|
||||||
setPage(await context.newPage());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auto-accept native browser dialogs (confirm/alert from 1C scripts like vis.js)
|
|
||||||
page.on('dialog', dialog => dialog.accept().catch(() => {}));
|
|
||||||
|
|
||||||
// Capture seanceId from network requests for graceful logout
|
|
||||||
setSessionPrefix(null);
|
|
||||||
setSeanceId(null);
|
|
||||||
page.on('request', req => {
|
|
||||||
if (seanceId) return;
|
|
||||||
const m = req.url().match(/^(https?:\/\/[^/]+\/[^/]+\/[^/]+)\/e1cib\/.+[?&]seanceId=([^&]+)/);
|
|
||||||
if (m) { setSessionPrefix(m[1]); setSeanceId(m[2]); }
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: LOAD_TIMEOUT });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for 1C to initialize — detect by section panel appearance
|
|
||||||
try {
|
|
||||||
await page.waitForSelector('#themesCell_theme_0', { timeout: INIT_TIMEOUT });
|
|
||||||
} catch {
|
|
||||||
// Fallback: wait fixed time if selector doesn't appear (e.g. login page)
|
|
||||||
await page.waitForTimeout(5000);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to close startup modals (Путеводитель etc.)
|
|
||||||
await closeModals();
|
|
||||||
|
|
||||||
return await getPageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Best-effort POST /e1cib/logout on a slot to release the 1C session license.
|
|
||||||
* Silent — if page is closed or session info missing, just returns.
|
|
||||||
* @param {object} slot { page, sessionPrefix, seanceId } from contexts Map
|
|
||||||
* @param {number} [waitMs=500] pause after logout fetch (gives 1C time to process)
|
|
||||||
*/
|
|
||||||
async function logoutSlot(slot, waitMs = 500) {
|
|
||||||
if (!slot?.page || slot.page.isClosed() || !slot.seanceId || !slot.sessionPrefix) return;
|
|
||||||
try {
|
|
||||||
const logoutUrl = `${slot.sessionPrefix}/e1cib/logout?seanceId=${slot.seanceId}`;
|
|
||||||
await slot.page.evaluate(async (url) => {
|
|
||||||
await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{"root":{}}' });
|
|
||||||
}, logoutUrl);
|
|
||||||
await slot.page.waitForTimeout(waitMs);
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gracefully terminate the 1C session and close the browser.
|
|
||||||
* Sends POST /e1cib/logout to release the license before closing.
|
|
||||||
*/
|
|
||||||
export async function disconnect() {
|
|
||||||
// Multi-context path: stop recording + logout each slot before closing browser
|
|
||||||
if (contexts.size > 0) {
|
|
||||||
saveActiveSlot();
|
|
||||||
// Recorder is global — one stop covers all contexts
|
|
||||||
if (recorder) {
|
|
||||||
try { await stopRecording(); } catch {}
|
|
||||||
}
|
|
||||||
for (const [, slot] of contexts.entries()) {
|
|
||||||
await logoutSlot(slot);
|
|
||||||
}
|
|
||||||
contexts.clear();
|
|
||||||
setActiveContextName(null);
|
|
||||||
setActiveMode(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Single-session path (connect): auto-stop recording if active
|
|
||||||
if (recorder) {
|
|
||||||
try { await stopRecording(); } catch {}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (browser) {
|
|
||||||
// Graceful logout — release the 1C license (single-session connect path)
|
|
||||||
await logoutSlot({ page, sessionPrefix, seanceId }, 1000);
|
|
||||||
await browser.close().catch(() => {});
|
|
||||||
setBrowser(null);
|
|
||||||
setPage(null);
|
|
||||||
setSessionPrefix(null);
|
|
||||||
setSeanceId(null);
|
|
||||||
// Clean up persistent user data dir
|
|
||||||
if (persistentUserDataDir) {
|
|
||||||
try { rmSync(persistentUserDataDir, { recursive: true, force: true }); } catch {}
|
|
||||||
setPersistentUserDataDir(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attach to a running browser server via CDP WebSocket.
|
|
||||||
* Sets module state so all functions (getFormState, clickElement, etc.) work.
|
|
||||||
*/
|
|
||||||
export async function attach(wsEndpoint, session = {}) {
|
|
||||||
if (isConnected()) return;
|
|
||||||
setBrowser(await chromium.connect(wsEndpoint));
|
|
||||||
const ctx = browser.contexts()[0];
|
|
||||||
setPage(ctx?.pages()[0]);
|
|
||||||
if (!page) throw new Error('No page found in browser');
|
|
||||||
setSessionPrefix(session.sessionPrefix || null);
|
|
||||||
setSeanceId(session.seanceId || null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Detach from browser without closing it.
|
|
||||||
* Returns session state for persistence.
|
|
||||||
*/
|
|
||||||
export function detach() {
|
|
||||||
const session = { sessionPrefix, seanceId };
|
|
||||||
setBrowser(null);
|
|
||||||
setPage(null);
|
|
||||||
setSessionPrefix(null);
|
|
||||||
setSeanceId(null);
|
|
||||||
return session;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Get current session state (for saving between reconnections). */
|
|
||||||
export function getSession() {
|
|
||||||
return { sessionPrefix, seanceId };
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// Multi-context support (used by run.mjs cmdTest only)
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Save current module-level state into the active slot before switching.
|
|
||||||
* No-op if no active slot.
|
|
||||||
*/
|
|
||||||
function saveActiveSlot() {
|
|
||||||
if (!activeContextName) return;
|
|
||||||
const slot = contexts.get(activeContextName);
|
|
||||||
if (!slot) return;
|
|
||||||
slot.page = page;
|
|
||||||
slot.sessionPrefix = sessionPrefix;
|
|
||||||
slot.seanceId = seanceId;
|
|
||||||
slot.highlightMode = highlightMode;
|
|
||||||
// Note: `recorder`, `lastCaptions`, `lastRecordingDuration` are intentionally NOT
|
|
||||||
// mirrored per-slot. A multi-context recording produces one continuous output file —
|
|
||||||
// the recorder follows the active page via recorder._attachPage(), not per-slot state.
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Load a slot's state into module-level vars and mark it active. */
|
|
||||||
function activateSlot(name) {
|
|
||||||
const slot = contexts.get(name);
|
|
||||||
if (!slot) throw new Error(`Context "${name}" not found. Create it via createContext() first.`);
|
|
||||||
setPage(slot.page);
|
|
||||||
setSessionPrefix(slot.sessionPrefix);
|
|
||||||
setSeanceId(slot.seanceId);
|
|
||||||
setHighlightMode(slot.highlightMode || false);
|
|
||||||
setActiveContextName(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Attach 1C session listeners to a page, writing into the given slot. */
|
|
||||||
function attachSessionListeners(pg, slot, name) {
|
|
||||||
pg.on('dialog', dialog => dialog.accept().catch(() => {}));
|
|
||||||
pg.on('request', req => {
|
|
||||||
if (slot.seanceId) return;
|
|
||||||
const m = req.url().match(/^(https?:\/\/[^/]+\/[^/]+\/[^/]+)\/e1cib\/.+[?&]seanceId=([^&]+)/);
|
|
||||||
if (m) {
|
|
||||||
slot.sessionPrefix = m[1];
|
|
||||||
slot.seanceId = m[2];
|
|
||||||
if (activeContextName === name) {
|
|
||||||
setSessionPrefix(m[1]);
|
|
||||||
setSeanceId(m[2]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create (or navigate) a named browser context.
|
|
||||||
* First call launches Chromium via chromium.launch() (NOT launchPersistentContext) so that
|
|
||||||
* subsequent calls can create additional isolated BrowserContexts in the same process.
|
|
||||||
* Trade-off: 1C browser extension is loaded via --load-extension (process-level) rather than
|
|
||||||
* persistent profile.
|
|
||||||
*
|
|
||||||
* Use this from run.mjs cmdTest only — exec/run/start use connect() and stay on the
|
|
||||||
* legacy persistent-context path.
|
|
||||||
*/
|
|
||||||
export async function createContext(name, url, { extensionPath, isolation = 'tab' } = {}) {
|
|
||||||
if (contexts.has(name)) {
|
|
||||||
await setActiveContext(name);
|
|
||||||
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: LOAD_TIMEOUT });
|
|
||||||
try { await page.waitForSelector('#themesCell_theme_0', { timeout: INIT_TIMEOUT }); }
|
|
||||||
catch { await page.waitForTimeout(5000); }
|
|
||||||
await closeModals();
|
|
||||||
return await getPageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!['tab', 'window'].includes(isolation)) {
|
|
||||||
throw new Error(`createContext: invalid isolation "${isolation}", expected 'tab' or 'window'`);
|
|
||||||
}
|
|
||||||
if (activeMode && activeMode !== isolation) {
|
|
||||||
throw new Error(`createContext: cannot mix isolation modes — first context used "${activeMode}", "${name}" requested "${isolation}". Use the same mode for all contexts in one run.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// First context: launch browser. Subsequent: reuse existing.
|
|
||||||
let isFirstContext = !browser;
|
|
||||||
if (isFirstContext) {
|
|
||||||
const extPath = findExtension(extensionPath);
|
|
||||||
const launchArgs = ['--start-maximized'];
|
|
||||||
if (extPath) {
|
|
||||||
launchArgs.push('--disable-extensions-except=' + extPath, '--load-extension=' + extPath);
|
|
||||||
}
|
|
||||||
if (isolation === 'tab') {
|
|
||||||
// Persistent context: extension loads reliably, one window with tabs per context
|
|
||||||
setPersistentUserDataDir(pathJoin(tmpdir(), 'pw-1c-test-' + Date.now()));
|
|
||||||
mkdirSync(persistentUserDataDir, { recursive: true });
|
|
||||||
setBrowser(await chromium.launchPersistentContext(persistentUserDataDir, {
|
|
||||||
headless: false,
|
|
||||||
args: launchArgs,
|
|
||||||
viewport: null,
|
|
||||||
permissions: ['clipboard-read', 'clipboard-write'],
|
|
||||||
}));
|
|
||||||
} else {
|
|
||||||
// Window mode: separate BrowserContext per slot, full cookie isolation
|
|
||||||
setBrowser(await chromium.launch({ headless: false, args: launchArgs }));
|
|
||||||
}
|
|
||||||
setActiveMode(isolation);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save current active before switching
|
|
||||||
saveActiveSlot();
|
|
||||||
|
|
||||||
// Create slot — page differs by mode
|
|
||||||
let newCtx, newPage;
|
|
||||||
if (activeMode === 'tab') {
|
|
||||||
// Reuse the persistent context for all slots; each slot gets its own page (tab)
|
|
||||||
newCtx = browser;
|
|
||||||
if (isFirstContext) {
|
|
||||||
newPage = browser.pages()[0] || await browser.newPage();
|
|
||||||
} else {
|
|
||||||
newPage = await browser.newPage();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Window mode: each slot owns its BrowserContext + page
|
|
||||||
newCtx = await browser.newContext({
|
|
||||||
viewport: null,
|
|
||||||
permissions: ['clipboard-read', 'clipboard-write'],
|
|
||||||
});
|
|
||||||
newPage = await newCtx.newPage();
|
|
||||||
}
|
|
||||||
|
|
||||||
const slot = {
|
|
||||||
context: newCtx,
|
|
||||||
page: newPage,
|
|
||||||
sessionPrefix: null,
|
|
||||||
seanceId: null,
|
|
||||||
highlightMode: false,
|
|
||||||
};
|
|
||||||
contexts.set(name, slot);
|
|
||||||
|
|
||||||
attachSessionListeners(newPage, slot, name);
|
|
||||||
activateSlot(name);
|
|
||||||
|
|
||||||
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: LOAD_TIMEOUT });
|
|
||||||
try { await page.waitForSelector('#themesCell_theme_0', { timeout: INIT_TIMEOUT }); }
|
|
||||||
catch { await page.waitForTimeout(5000); }
|
|
||||||
await closeModals();
|
|
||||||
|
|
||||||
return await getPageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Switch the active context. Subsequent browser API calls operate on this context's page. */
|
|
||||||
export async function setActiveContext(name) {
|
|
||||||
if (activeContextName === name) return;
|
|
||||||
if (!contexts.has(name)) throw new Error(`Context "${name}" not found. Available: [${[...contexts.keys()].join(', ')}]`);
|
|
||||||
// If a recording is active, flush the outgoing page's last frame so the gap is filled
|
|
||||||
// up to the moment of the switch (avoids a "jump" in video time).
|
|
||||||
if (recorder && recorder._flushFrames) recorder._flushFrames();
|
|
||||||
saveActiveSlot();
|
|
||||||
activateSlot(name);
|
|
||||||
// If the recording is still alive (it lives across slots — we keep the same ffmpeg/output),
|
|
||||||
// re-attach its screencast to the newly active page.
|
|
||||||
if (recorder && recorder._attachPage) {
|
|
||||||
await recorder._attachPage(page);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function listContexts() {
|
|
||||||
return [...contexts.keys()];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getActiveContext() {
|
|
||||||
return activeContextName;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function hasContext(name) {
|
|
||||||
return contexts.has(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Close a named context: logout, close its page (tab mode) or BrowserContext
|
|
||||||
* (window mode), remove from registry. Cannot close the currently active
|
|
||||||
* context — caller must setActiveContext to another first. This keeps the
|
|
||||||
* recorder/page invariants simple: recorder is always attached to the
|
|
||||||
* active slot, which closeContext never touches.
|
|
||||||
*
|
|
||||||
* @throws if name is not registered or equals the active context.
|
|
||||||
*/
|
|
||||||
export async function closeContext(name) {
|
|
||||||
if (!contexts.has(name)) {
|
|
||||||
throw new Error(`Context "${name}" not found. Available: [${[...contexts.keys()].join(', ')}]`);
|
|
||||||
}
|
|
||||||
if (name === activeContextName) {
|
|
||||||
throw new Error(`closeContext: cannot close the active context "${name}". setActiveContext to another context first.`);
|
|
||||||
}
|
|
||||||
const slot = contexts.get(name);
|
|
||||||
await logoutSlot(slot);
|
|
||||||
if (activeMode === 'tab') {
|
|
||||||
try { await slot.page.close(); } catch {}
|
|
||||||
} else {
|
|
||||||
try { await slot.context.close(); } catch {}
|
|
||||||
}
|
|
||||||
contexts.delete(name);
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
// web-test forms/close v1.18 — Close current form via Escape, handle save-changes confirmation.
|
|
||||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
|
|
||||||
import { page, recorder, ensureConnected } from '../core/state.mjs';
|
|
||||||
import { detectFormScript } from '../../dom.mjs';
|
|
||||||
import { dismissPendingErrors, checkForErrors, detectPlatformDialogs, closePlatformDialogs } from '../core/errors.mjs';
|
|
||||||
import { waitForStable } from '../core/wait.mjs';
|
|
||||||
import { returnFormState } from '../core/helpers.mjs';
|
|
||||||
import { getFormState } from './state.mjs';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Close the current form/dialog via Escape.
|
|
||||||
* @param {Object} [opts]
|
|
||||||
* @param {boolean} [opts.save] - Handle "Save changes?" confirmation automatically:
|
|
||||||
* true → click "Да" (save and close)
|
|
||||||
* false → click "Нет" (discard and close)
|
|
||||||
* undefined → return confirmation as hint for caller to decide
|
|
||||||
*/
|
|
||||||
export async function closeForm({ save } = {}) {
|
|
||||||
ensureConnected();
|
|
||||||
await dismissPendingErrors();
|
|
||||||
// If platform dialogs are open, close them instead of pressing Escape
|
|
||||||
const pd = await detectPlatformDialogs();
|
|
||||||
if (pd.length) {
|
|
||||||
await closePlatformDialogs();
|
|
||||||
await page.waitForTimeout(300);
|
|
||||||
return returnFormState({ closed: true, closedPlatformDialogs: pd });
|
|
||||||
}
|
|
||||||
const beforeForm = await page.evaluate(detectFormScript());
|
|
||||||
await page.keyboard.press('Escape');
|
|
||||||
await waitForStable(beforeForm);
|
|
||||||
const state = await getFormState();
|
|
||||||
const err = await checkForErrors();
|
|
||||||
if (err?.confirmation) {
|
|
||||||
if (save === true || save === false) {
|
|
||||||
const label = save ? 'Да' : 'Нет';
|
|
||||||
const btnSel = `#form${err.confirmation.formNum}_container a.press.pressButton`;
|
|
||||||
const btns = await page.$$(btnSel);
|
|
||||||
for (const b of btns) {
|
|
||||||
const txt = (await b.textContent()).trim();
|
|
||||||
if (txt === label) {
|
|
||||||
if (recorder) await page.waitForTimeout(500); // show confirmation to viewer during recording
|
|
||||||
await b.click({ force: true });
|
|
||||||
await waitForStable(beforeForm);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const afterForm = await page.evaluate(detectFormScript());
|
|
||||||
return returnFormState({ closed: afterForm !== beforeForm });
|
|
||||||
}
|
|
||||||
state.confirmation = err.confirmation;
|
|
||||||
state.hint = 'Confirmation dialog shown. Click "Да" to confirm or "Нет" to cancel';
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
return returnFormState({ closed: state.form !== beforeForm });
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
# Коммиты, которые git blame должен «проскакивать» (механические правки —
|
|
||||||
# не меняют авторство содержимого). Включить локально:
|
|
||||||
# git config blame.ignoreRevsFile .git-blame-ignore-revs
|
|
||||||
# GitHub/GitLab уважают этот файл в blame-UI автоматически.
|
|
||||||
|
|
||||||
# chore(repo): нормализация EOL к LF + .gitattributes
|
|
||||||
26888a07d58351755fb8e487727c00d5b611eb95
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
# EOL policy
|
|
||||||
# ─────────────────────────────────────────────────────────────────────────────
|
|
||||||
# Авторский контент нормализуем к LF: инструмент правки (Edit) всегда пишет LF,
|
|
||||||
# поэтому единый LF убирает EOL-шум в диффах и ловушку «не правь CRLF-файл».
|
|
||||||
# git с eol=lf конвертит ТОЛЬКО CR<->LF и не трогает BOM (BOM — байты контента),
|
|
||||||
# поэтому BOM на .ps1 сохраняется.
|
|
||||||
*.ps1 text eol=lf
|
|
||||||
*.psm1 text eol=lf
|
|
||||||
*.py text eol=lf
|
|
||||||
*.mjs text eol=lf
|
|
||||||
*.md text eol=lf
|
|
||||||
*.json text eol=lf
|
|
||||||
.gitignore text eol=lf
|
|
||||||
|
|
||||||
# .bsl уже целиком LF — пин фиксирует статус-кво от будущего дрейфа.
|
|
||||||
*.bsl text eol=lf
|
|
||||||
|
|
||||||
# Данные 1С НЕ трогаем. *.xml — реальные выгрузки 1С (EOL местами значим,
|
|
||||||
# правим не мы, а навыки): оставляем как есть, под управление не берём.
|
|
||||||
# autocrlf=false и отсутствие text-атрибута => git хранит их байты как есть.
|
|
||||||
|
|
||||||
# Бинарники 1С
|
|
||||||
*.bin binary
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
# 1C Skills for {{PLATFORM_LABEL}} ({{RUNTIME_LABEL}})
|
|
||||||
|
|
||||||
Автоматическая сборка из [main]({{MAIN_REPO_URL}}) — навыки 1С:Предприятие 8.3 для AI-агента **{{PLATFORM_LABEL}}** с рантаймом **{{RUNTIME_LABEL}}**.
|
|
||||||
|
|
||||||
> Эта ветка генерируется CI на каждый push в main. **Не редактируйте напрямую** — все правки идут в [main]({{MAIN_REPO_URL}}).
|
|
||||||
|
|
||||||
## Установка
|
|
||||||
|
|
||||||
1. Скачайте ZIP этой ветки: **Code → Download ZIP** (или `git archive`).
|
|
||||||
2. Распакуйте в корень своего проекта — должна появиться папка `{{PLATFORM_DIR}}/`.
|
|
||||||
3. Запустите {{PLATFORM_LABEL}} из этого проекта — навыки станут доступны.
|
|
||||||
|
|
||||||
## Требования
|
|
||||||
|
|
||||||
- **Windows** с PowerShell 5.1+ (входит в Windows) — для PowerShell-сборки.
|
|
||||||
- **Python 3.10+** — для Python-сборки. Зависимости: `lxml>=4.9.0`, `psutil>=5.9.0` (для DOM- и web-навыков).
|
|
||||||
- **1С:Предприятие 8.3** — для сборки/разборки EPF/ERF и работы с базами.
|
|
||||||
- **Node.js 18+** — для `/web-test`.
|
|
||||||
|
|
||||||
## Документация
|
|
||||||
|
|
||||||
Полные гайды, спецификации и описание навыков — в [main]({{MAIN_REPO_URL}}).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Source: {{MAIN_REPO_URL}}
|
|
||||||
Build commit: `{{COMMIT_SHA}}`
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
|
|
||||||
"name": "{{PLUGIN_NAME}}",
|
|
||||||
"description": "[Python] Навыки для разработки на 1С:Предприятие 8.3 — абстракции над XML-форматами и CLI конфигуратора, плюс глаза и руки для тестирования через веб-клиент. Linux/Mac или когда PowerShell недоступен.",
|
|
||||||
"author": {
|
|
||||||
"name": "Nikolay Shirokov"
|
|
||||||
},
|
|
||||||
"homepage": "https://github.com/Nikolay-Shirokov/cc-1c-skills",
|
|
||||||
"repository": "https://github.com/Nikolay-Shirokov/cc-1c-skills",
|
|
||||||
"license": "MIT",
|
|
||||||
"keywords": [
|
|
||||||
"1c",
|
|
||||||
"1c-dev",
|
|
||||||
"cf",
|
|
||||||
"cfe",
|
|
||||||
"epf",
|
|
||||||
"erf",
|
|
||||||
"metadata",
|
|
||||||
"configuration",
|
|
||||||
"extension",
|
|
||||||
"form",
|
|
||||||
"report",
|
|
||||||
"skd",
|
|
||||||
"data-processor",
|
|
||||||
"mxl",
|
|
||||||
"web-client",
|
|
||||||
"testing",
|
|
||||||
"test-automation"
|
|
||||||
],
|
|
||||||
"skills": "./.claude/skills/"
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "{{PLUGIN_NAME}}",
|
|
||||||
"version": "{{VERSION}}",
|
|
||||||
"description": "[{{RUNTIME_LABEL}}] Навыки для разработки на 1С:Предприятие 8.3 — абстракции над XML-форматами и CLI конфигуратора, плюс глаза и руки для тестирования через веб-клиент.",
|
|
||||||
"author": {
|
|
||||||
"name": "Nikolay Shirokov"
|
|
||||||
},
|
|
||||||
"homepage": "https://github.com/Nikolay-Shirokov/cc-1c-skills",
|
|
||||||
"repository": "https://github.com/Nikolay-Shirokov/cc-1c-skills",
|
|
||||||
"license": "MIT",
|
|
||||||
"keywords": [
|
|
||||||
"1c",
|
|
||||||
"1c-dev",
|
|
||||||
"cf",
|
|
||||||
"cfe",
|
|
||||||
"epf",
|
|
||||||
"erf",
|
|
||||||
"metadata",
|
|
||||||
"configuration",
|
|
||||||
"extension",
|
|
||||||
"form",
|
|
||||||
"report",
|
|
||||||
"skd",
|
|
||||||
"data-processor",
|
|
||||||
"mxl",
|
|
||||||
"web-client",
|
|
||||||
"testing",
|
|
||||||
"test-automation"
|
|
||||||
],
|
|
||||||
"skills": "./.codex/skills/",
|
|
||||||
"interface": {
|
|
||||||
"displayName": "1C Skills ({{RUNTIME_LABEL}})",
|
|
||||||
"shortDescription": "{{SHORT_DESCRIPTION}}",
|
|
||||||
"category": "Development"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,234 +0,0 @@
|
|||||||
name: Build port branches
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [main]
|
|
||||||
paths:
|
|
||||||
- '.claude/skills/**'
|
|
||||||
- 'scripts/switch.py'
|
|
||||||
- '.github/templates/README.port.md.tmpl'
|
|
||||||
- '.github/templates/codex-plugin.json.tmpl'
|
|
||||||
- '.github/templates/claude-plugin.json.tmpl'
|
|
||||||
- '.github/workflows/build-ports.yml'
|
|
||||||
- 'LICENSE'
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
- platform: claude-code
|
|
||||||
runtime: python
|
|
||||||
branch: port-claude-code-py
|
|
||||||
label: Claude Code
|
|
||||||
target_dir: .claude/skills
|
|
||||||
- platform: cursor
|
|
||||||
runtime: powershell
|
|
||||||
branch: port-cursor
|
|
||||||
label: Cursor
|
|
||||||
target_dir: .cursor/skills
|
|
||||||
- platform: cursor
|
|
||||||
runtime: python
|
|
||||||
branch: port-cursor-py
|
|
||||||
label: Cursor
|
|
||||||
target_dir: .cursor/skills
|
|
||||||
- platform: codex
|
|
||||||
runtime: powershell
|
|
||||||
branch: port-codex
|
|
||||||
label: Codex
|
|
||||||
target_dir: .codex/skills
|
|
||||||
- platform: codex
|
|
||||||
runtime: python
|
|
||||||
branch: port-codex-py
|
|
||||||
label: Codex
|
|
||||||
target_dir: .codex/skills
|
|
||||||
- platform: copilot
|
|
||||||
runtime: powershell
|
|
||||||
branch: port-copilot
|
|
||||||
label: GitHub Copilot
|
|
||||||
target_dir: .github/skills
|
|
||||||
- platform: copilot
|
|
||||||
runtime: python
|
|
||||||
branch: port-copilot-py
|
|
||||||
label: GitHub Copilot
|
|
||||||
target_dir: .github/skills
|
|
||||||
- platform: augment
|
|
||||||
runtime: powershell
|
|
||||||
branch: port-augment
|
|
||||||
label: Augment
|
|
||||||
target_dir: .augment/skills
|
|
||||||
- platform: augment
|
|
||||||
runtime: python
|
|
||||||
branch: port-augment-py
|
|
||||||
label: Augment
|
|
||||||
target_dir: .augment/skills
|
|
||||||
- platform: cline
|
|
||||||
runtime: powershell
|
|
||||||
branch: port-cline
|
|
||||||
label: Cline
|
|
||||||
target_dir: .cline/skills
|
|
||||||
- platform: cline
|
|
||||||
runtime: python
|
|
||||||
branch: port-cline-py
|
|
||||||
label: Cline
|
|
||||||
target_dir: .cline/skills
|
|
||||||
- platform: kilo
|
|
||||||
runtime: powershell
|
|
||||||
branch: port-kilo
|
|
||||||
label: Kilo Code
|
|
||||||
target_dir: .kilocode/skills
|
|
||||||
- platform: kilo
|
|
||||||
runtime: python
|
|
||||||
branch: port-kilo-py
|
|
||||||
label: Kilo Code
|
|
||||||
target_dir: .kilocode/skills
|
|
||||||
- platform: kiro
|
|
||||||
runtime: powershell
|
|
||||||
branch: port-kiro
|
|
||||||
label: Kiro
|
|
||||||
target_dir: .kiro/skills
|
|
||||||
- platform: kiro
|
|
||||||
runtime: python
|
|
||||||
branch: port-kiro-py
|
|
||||||
label: Kiro
|
|
||||||
target_dir: .kiro/skills
|
|
||||||
- platform: gemini
|
|
||||||
runtime: powershell
|
|
||||||
branch: port-gemini
|
|
||||||
label: Gemini CLI
|
|
||||||
target_dir: .gemini/skills
|
|
||||||
- platform: gemini
|
|
||||||
runtime: python
|
|
||||||
branch: port-gemini-py
|
|
||||||
label: Gemini CLI
|
|
||||||
target_dir: .gemini/skills
|
|
||||||
- platform: opencode
|
|
||||||
runtime: powershell
|
|
||||||
branch: port-opencode
|
|
||||||
label: OpenCode
|
|
||||||
target_dir: .opencode/skills
|
|
||||||
- platform: opencode
|
|
||||||
runtime: python
|
|
||||||
branch: port-opencode-py
|
|
||||||
label: OpenCode
|
|
||||||
target_dir: .opencode/skills
|
|
||||||
- platform: roo
|
|
||||||
runtime: powershell
|
|
||||||
branch: port-roo
|
|
||||||
label: Roo Code
|
|
||||||
target_dir: .roo/skills
|
|
||||||
- platform: roo
|
|
||||||
runtime: python
|
|
||||||
branch: port-roo-py
|
|
||||||
label: Roo Code
|
|
||||||
target_dir: .roo/skills
|
|
||||||
- platform: windsurf
|
|
||||||
runtime: powershell
|
|
||||||
branch: port-windsurf
|
|
||||||
label: Windsurf
|
|
||||||
target_dir: .windsurf/skills
|
|
||||||
- platform: windsurf
|
|
||||||
runtime: python
|
|
||||||
branch: port-windsurf-py
|
|
||||||
label: Windsurf
|
|
||||||
target_dir: .windsurf/skills
|
|
||||||
- platform: codeassistant
|
|
||||||
runtime: powershell
|
|
||||||
branch: port-codeassistant
|
|
||||||
label: Yandex Code Assistant
|
|
||||||
target_dir: .codeassistant/skills
|
|
||||||
- platform: codeassistant
|
|
||||||
runtime: python
|
|
||||||
branch: port-codeassistant-py
|
|
||||||
label: Yandex Code Assistant
|
|
||||||
target_dir: .codeassistant/skills
|
|
||||||
- platform: agents
|
|
||||||
runtime: powershell
|
|
||||||
branch: port-agents
|
|
||||||
label: Agent Skills
|
|
||||||
target_dir: .agents/skills
|
|
||||||
- platform: agents
|
|
||||||
runtime: python
|
|
||||||
branch: port-agents-py
|
|
||||||
label: Agent Skills
|
|
||||||
target_dir: .agents/skills
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout main
|
|
||||||
uses: actions/checkout@v5
|
|
||||||
|
|
||||||
- name: Set up Python
|
|
||||||
uses: actions/setup-python@v6
|
|
||||||
with:
|
|
||||||
python-version: '3.11'
|
|
||||||
|
|
||||||
- name: Build skills tree for ${{ matrix.platform }} (${{ matrix.runtime }})
|
|
||||||
run: |
|
|
||||||
python scripts/switch.py "${{ matrix.platform }}" \
|
|
||||||
--project-dir build \
|
|
||||||
--runtime "${{ matrix.runtime }}"
|
|
||||||
|
|
||||||
- name: Render port README
|
|
||||||
env:
|
|
||||||
PLATFORM_LABEL: ${{ matrix.label }}
|
|
||||||
PLATFORM_DIR: ${{ matrix.target_dir }}
|
|
||||||
RUNTIME_LABEL: ${{ matrix.runtime == 'powershell' && 'PowerShell' || 'Python' }}
|
|
||||||
COMMIT_SHA: ${{ github.sha }}
|
|
||||||
MAIN_REPO_URL: https://github.com/${{ github.repository }}
|
|
||||||
run: |
|
|
||||||
sed \
|
|
||||||
-e "s|{{PLATFORM_LABEL}}|${PLATFORM_LABEL}|g" \
|
|
||||||
-e "s|{{PLATFORM_DIR}}|${PLATFORM_DIR}|g" \
|
|
||||||
-e "s|{{RUNTIME_LABEL}}|${RUNTIME_LABEL}|g" \
|
|
||||||
-e "s|{{COMMIT_SHA}}|${COMMIT_SHA}|g" \
|
|
||||||
-e "s|{{MAIN_REPO_URL}}|${MAIN_REPO_URL}|g" \
|
|
||||||
.github/templates/README.port.md.tmpl > build/README.md
|
|
||||||
|
|
||||||
- name: Render Codex plugin manifest
|
|
||||||
if: matrix.platform == 'codex'
|
|
||||||
env:
|
|
||||||
PLUGIN_NAME: ${{ matrix.runtime == 'python' && '1c-skills-py' || '1c-skills' }}
|
|
||||||
RUNTIME_LABEL: ${{ matrix.runtime == 'powershell' && 'PowerShell' || 'Python' }}
|
|
||||||
SHORT_DESCRIPTION: ${{ matrix.runtime == 'python' && 'Python runtime (Linux/Mac/Windows)' || 'PowerShell runtime (Windows-first)' }}
|
|
||||||
COMMIT_SHA: ${{ github.sha }}
|
|
||||||
run: |
|
|
||||||
VERSION="$(date -u +%Y.%-m.%-d)+${COMMIT_SHA::7}"
|
|
||||||
mkdir -p build/.codex-plugin
|
|
||||||
sed \
|
|
||||||
-e "s|{{PLUGIN_NAME}}|${PLUGIN_NAME}|g" \
|
|
||||||
-e "s|{{VERSION}}|${VERSION}|g" \
|
|
||||||
-e "s|{{RUNTIME_LABEL}}|${RUNTIME_LABEL}|g" \
|
|
||||||
-e "s|{{SHORT_DESCRIPTION}}|${SHORT_DESCRIPTION}|g" \
|
|
||||||
.github/templates/codex-plugin.json.tmpl > build/.codex-plugin/plugin.json
|
|
||||||
|
|
||||||
- name: Render Claude plugin manifest (Py variant)
|
|
||||||
if: matrix.platform == 'claude-code' && matrix.runtime == 'python'
|
|
||||||
env:
|
|
||||||
PLUGIN_NAME: 1c-skills-py
|
|
||||||
run: |
|
|
||||||
mkdir -p build/.claude-plugin
|
|
||||||
sed -e "s|{{PLUGIN_NAME}}|${PLUGIN_NAME}|g" \
|
|
||||||
.github/templates/claude-plugin.json.tmpl > build/.claude-plugin/plugin.json
|
|
||||||
|
|
||||||
- name: Copy LICENSE
|
|
||||||
run: cp LICENSE build/LICENSE
|
|
||||||
|
|
||||||
- name: Force-push orphan snapshot to ${{ matrix.branch }}
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
run: |
|
|
||||||
cd build
|
|
||||||
git init -q -b master
|
|
||||||
git config user.name "github-actions[bot]"
|
|
||||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
||||||
git add -A
|
|
||||||
git commit -q -m "Auto-build: ${{ matrix.platform }} (${{ matrix.runtime }}) from ${GITHUB_SHA::7}"
|
|
||||||
git push --force \
|
|
||||||
"https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git" \
|
|
||||||
"master:${{ matrix.branch }}"
|
|
||||||
-52
@@ -1,52 +0,0 @@
|
|||||||
# Реальные выгрузки обработок (примеры, не для версионирования)
|
|
||||||
upload/
|
|
||||||
|
|
||||||
# Результаты сборки
|
|
||||||
build/
|
|
||||||
base/
|
|
||||||
*.epf
|
|
||||||
*.log
|
|
||||||
|
|
||||||
# Временные файлы тестов
|
|
||||||
test-tmp/
|
|
||||||
|
|
||||||
# Локальные настройки Claude Code
|
|
||||||
.claude/settings.local.json
|
|
||||||
|
|
||||||
# Инструменты (portable Apache и т.д.)
|
|
||||||
tools/
|
|
||||||
|
|
||||||
# Отладка навыков (eval, trigger-test, run_loop результаты)
|
|
||||||
debug/
|
|
||||||
|
|
||||||
# Кэш тестов навыков
|
|
||||||
tests/skills/.cache/
|
|
||||||
|
|
||||||
# Python кэш
|
|
||||||
__pycache__/
|
|
||||||
|
|
||||||
# Локальный реестр баз данных 1С
|
|
||||||
.v8-project.json
|
|
||||||
|
|
||||||
# web-test: Node.js зависимости и runtime-артефакты
|
|
||||||
.claude/skills/web-test/scripts/node_modules/
|
|
||||||
.claude/skills/web-test/.browser-session.json
|
|
||||||
|
|
||||||
# Скриншоты и видео (артефакты тестирования web-test)
|
|
||||||
*.png
|
|
||||||
*.mp4
|
|
||||||
|
|
||||||
# Навыки, скопированные для других AI-платформ (генерируются scripts/switch.py)
|
|
||||||
.agents/skills/
|
|
||||||
.augment/
|
|
||||||
.cline/
|
|
||||||
.codex/
|
|
||||||
.cursor/
|
|
||||||
.gemini/
|
|
||||||
.github/skills/
|
|
||||||
.kilocode/
|
|
||||||
.kiro/
|
|
||||||
.opencode/
|
|
||||||
.roo/
|
|
||||||
.windsurf/
|
|
||||||
debug-templates.txt
|
|
||||||
@@ -24,7 +24,7 @@ allowed-tools:
|
|||||||
| `NoValidate` | Пропустить авто-валидацию |
|
| `NoValidate` | Пропустить авто-валидацию |
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-edit.ps1" -ConfigPath '<path>' -Operation modify-property -Value 'Version=1.0.0.1'
|
powershell.exe -NoProfile -File ".opencode/skills/cf-edit/scripts/cf-edit.ps1" -ConfigPath '<path>' -Operation modify-property -Value 'Version=1.0.0.1'
|
||||||
```
|
```
|
||||||
|
|
||||||
## Операции
|
## Операции
|
||||||
+14
-1
@@ -1,4 +1,4 @@
|
|||||||
# cf-edit v1.8 — Edit 1C configuration root (Configuration.xml)
|
# cf-edit v1.9 — Edit 1C configuration root (Configuration.xml)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)][Alias('Path')][string]$ConfigPath,
|
[Parameter(Mandatory)][Alias('Path')][string]$ConfigPath,
|
||||||
@@ -44,6 +44,16 @@ function Get-RootUuid([string]$xmlPath) {
|
|||||||
} catch {}
|
} catch {}
|
||||||
return $null
|
return $null
|
||||||
}
|
}
|
||||||
|
function Test-ExternalObjectRoot([string]$xmlPath) {
|
||||||
|
if (-not (Test-Path $xmlPath)) { return $false }
|
||||||
|
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) { return @('ExternalDataProcessor','ExternalReport') -contains $el.LocalName }
|
||||||
|
} catch {}
|
||||||
|
return $false
|
||||||
|
}
|
||||||
function Find-V8Project([string]$startDir) {
|
function Find-V8Project([string]$startDir) {
|
||||||
$d = $startDir
|
$d = $startDir
|
||||||
for ($i = 0; $i -lt 20 -and $d; $i++) {
|
for ($i = 0; $i -lt 20 -and $d; $i++) {
|
||||||
@@ -80,10 +90,13 @@ function Assert-EditAllowed([string]$targetPath, [string]$require) {
|
|||||||
try {
|
try {
|
||||||
$rp = $targetPath
|
$rp = $targetPath
|
||||||
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
|
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
|
||||||
|
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
|
||||||
|
if (Test-ExternalObjectRoot $rp) { return }
|
||||||
$elemUuid = Get-RootUuid $rp
|
$elemUuid = Get-RootUuid $rp
|
||||||
$cfgDir = $null; $binPath = $null
|
$cfgDir = $null; $binPath = $null
|
||||||
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
|
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
|
||||||
for ($i = 0; $i -lt 12 -and $d; $i++) {
|
for ($i = 0; $i -lt 12 -and $d; $i++) {
|
||||||
|
if (Test-ExternalObjectRoot "$d.xml") { return }
|
||||||
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
|
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
|
||||||
if (-not $cfgDir) {
|
if (-not $cfgDir) {
|
||||||
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
|
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
|
||||||
+18
-1
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# cf-edit v1.8 — Edit 1C configuration root (Configuration.xml)
|
# cf-edit v1.9 — Edit 1C configuration root (Configuration.xml)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -33,6 +33,18 @@ def _sg_root_uuid(xml_path):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _sg_is_external_root(xml_path):
|
||||||
|
if not os.path.isfile(xml_path):
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
mx = etree.parse(xml_path).getroot()
|
||||||
|
for child in mx:
|
||||||
|
if isinstance(child.tag, str):
|
||||||
|
return child.tag.split("}")[-1] in ("ExternalDataProcessor", "ExternalReport")
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
return False
|
||||||
|
|
||||||
def _sg_find_v8project(start_dir):
|
def _sg_find_v8project(start_dir):
|
||||||
d = start_dir
|
d = start_dir
|
||||||
for _ in range(20):
|
for _ in range(20):
|
||||||
@@ -72,6 +84,9 @@ def _sg_get_edit_mode(cfg_dir):
|
|||||||
def assert_edit_allowed(target_path, require):
|
def assert_edit_allowed(target_path, require):
|
||||||
try:
|
try:
|
||||||
rp = os.path.abspath(target_path)
|
rp = os.path.abspath(target_path)
|
||||||
|
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
|
||||||
|
if _sg_is_external_root(rp):
|
||||||
|
return
|
||||||
elem_uuid = _sg_root_uuid(rp)
|
elem_uuid = _sg_root_uuid(rp)
|
||||||
cfg_dir = None
|
cfg_dir = None
|
||||||
bin_path = None
|
bin_path = None
|
||||||
@@ -79,6 +94,8 @@ def assert_edit_allowed(target_path, require):
|
|||||||
for _ in range(12):
|
for _ in range(12):
|
||||||
if not d:
|
if not d:
|
||||||
break
|
break
|
||||||
|
if _sg_is_external_root(d + ".xml"):
|
||||||
|
return
|
||||||
if not elem_uuid:
|
if not elem_uuid:
|
||||||
elem_uuid = _sg_root_uuid(d + ".xml")
|
elem_uuid = _sg_root_uuid(d + ".xml")
|
||||||
if not cfg_dir:
|
if not cfg_dir:
|
||||||
@@ -23,7 +23,7 @@ allowed-tools:
|
|||||||
| `OutFile` | Записать результат в файл (UTF-8 BOM) |
|
| `OutFile` | Записать результат в файл (UTF-8 BOM) |
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-info.ps1" -ConfigPath "<путь>"
|
powershell.exe -NoProfile -File ".opencode/skills/cf-info/scripts/cf-info.ps1" -ConfigPath "<путь>"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Три режима
|
## Три режима
|
||||||
@@ -24,7 +24,7 @@ allowed-tools:
|
|||||||
| `CompatibilityMode` | Режим совместимости (default: `Version8_3_24`) |
|
| `CompatibilityMode` | Режим совместимости (default: `Version8_3_24`) |
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-init.ps1" -Name "МояКонфигурация"
|
powershell.exe -NoProfile -File ".opencode/skills/cf-init/scripts/cf-init.ps1" -Name "МояКонфигурация"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Примеры
|
## Примеры
|
||||||
@@ -24,6 +24,6 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-validate.ps1" -ConfigPath "upload/cfempty"
|
powershell.exe -NoProfile -File ".opencode/skills/cf-validate/scripts/cf-validate.ps1" -ConfigPath "upload/cfempty"
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-validate.ps1" -ConfigPath "upload/cfempty/Configuration.xml"
|
powershell.exe -NoProfile -File ".opencode/skills/cf-validate/scripts/cf-validate.ps1" -ConfigPath "upload/cfempty/Configuration.xml"
|
||||||
```
|
```
|
||||||
@@ -71,7 +71,7 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-borrow.ps1" -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты"
|
powershell.exe -NoProfile -File ".opencode/skills/cfe-borrow/scripts/cfe-borrow.ps1" -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Примеры
|
## Примеры
|
||||||
@@ -23,7 +23,7 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-diff.ps1" -ExtensionPath src -ConfigPath C:\cfsrc\erp -Mode A
|
powershell.exe -NoProfile -File ".opencode/skills/cfe-diff/scripts/cfe-diff.ps1" -ExtensionPath src -ConfigPath C:\cfsrc\erp -Mode A
|
||||||
```
|
```
|
||||||
|
|
||||||
## Mode A — обзор расширения
|
## Mode A — обзор расширения
|
||||||
@@ -44,7 +44,7 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-init.ps1" -Name "МоёРасширение"
|
powershell.exe -NoProfile -File ".opencode/skills/cfe-init/scripts/cfe-init.ps1" -Name "МоёРасширение"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Примеры
|
## Примеры
|
||||||
+1
-1
@@ -51,7 +51,7 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-patch-method.ps1" -ExtensionPath src -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
|
powershell.exe -NoProfile -File ".opencode/skills/cfe-patch-method/scripts/cfe-patch-method.ps1" -ExtensionPath src -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
|
||||||
```
|
```
|
||||||
|
|
||||||
## Примеры
|
## Примеры
|
||||||
@@ -24,6 +24,6 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-validate.ps1" -ExtensionPath "src"
|
powershell.exe -NoProfile -File ".opencode/skills/cfe-validate/scripts/cfe-validate.ps1" -ExtensionPath "src"
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-validate.ps1" -ExtensionPath "src/Configuration.xml"
|
powershell.exe -NoProfile -File ".opencode/skills/cfe-validate/scripts/cfe-validate.ps1" -ExtensionPath "src/Configuration.xml"
|
||||||
```
|
```
|
||||||
@@ -31,7 +31,7 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" <параметры>
|
powershell.exe -NoProfile -File ".opencode/skills/db-create/scripts/db-create.ps1" <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
@@ -57,14 +57,14 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" <п
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Создать файловую базу
|
# Создать файловую базу
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" -InfoBasePath "C:\Bases\NewDB"
|
powershell.exe -NoProfile -File ".opencode/skills/db-create/scripts/db-create.ps1" -InfoBasePath "C:\Bases\NewDB"
|
||||||
|
|
||||||
# Создать серверную базу
|
# Создать серверную базу
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test"
|
powershell.exe -NoProfile -File ".opencode/skills/db-create/scripts/db-create.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test"
|
||||||
|
|
||||||
# Создать из шаблона CF
|
# Создать из шаблона CF
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" -InfoBasePath "C:\Bases\NewDB" -UseTemplate "C:\Templates\config.cf"
|
powershell.exe -NoProfile -File ".opencode/skills/db-create/scripts/db-create.ps1" -InfoBasePath "C:\Bases\NewDB" -UseTemplate "C:\Templates\config.cf"
|
||||||
|
|
||||||
# Создать и добавить в список баз
|
# Создать и добавить в список баз
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" -InfoBasePath "C:\Bases\NewDB" -AddToList -ListName "Новая база"
|
powershell.exe -NoProfile -File ".opencode/skills/db-create/scripts/db-create.ps1" -InfoBasePath "C:\Bases\NewDB" -AddToList -ListName "Новая база"
|
||||||
```
|
```
|
||||||
@@ -35,7 +35,7 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.ps1" <параметры>
|
powershell.exe -NoProfile -File ".opencode/skills/db-dump-cf/scripts/db-dump-cf.ps1" <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
@@ -58,11 +58,11 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.ps1" <п
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Выгрузка конфигурации (файловая база)
|
# Выгрузка конфигурации (файловая база)
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "C:\backup\config.cf"
|
powershell.exe -NoProfile -File ".opencode/skills/db-dump-cf/scripts/db-dump-cf.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "C:\backup\config.cf"
|
||||||
|
|
||||||
# Серверная база
|
# Серверная база
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -OutputFile "config.cf"
|
powershell.exe -NoProfile -File ".opencode/skills/db-dump-cf/scripts/db-dump-cf.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -OutputFile "config.cf"
|
||||||
|
|
||||||
# Выгрузка расширения
|
# Выгрузка расширения
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "ext.cfe" -Extension "МоёРасширение"
|
powershell.exe -NoProfile -File ".opencode/skills/db-dump-cf/scripts/db-dump-cf.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "ext.cfe" -Extension "МоёРасширение"
|
||||||
```
|
```
|
||||||
@@ -38,7 +38,7 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-dt.ps1" <параметры>
|
powershell.exe -NoProfile -File ".opencode/skills/db-dump-dt/scripts/db-dump-dt.ps1" <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
@@ -59,10 +59,10 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-dt.ps1" <п
|
|||||||
|
|
||||||
```powershell
|
```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 ".opencode/skills/db-dump-dt/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"
|
powershell.exe -NoProfile -File ".opencode/skills/db-dump-dt/scripts/db-dump-dt.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -OutputFile "base.dt"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Связанные навыки
|
## Связанные навыки
|
||||||
@@ -37,7 +37,7 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" <параметры>
|
powershell.exe -NoProfile -File ".opencode/skills/db-dump-xml/scripts/db-dump-xml.ps1" <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
@@ -74,17 +74,17 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" <
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Полная выгрузка (файловая база)
|
# Полная выгрузка (файловая база)
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" -V8Path "C:\Program Files\1cv8\8.3.25.1257\bin" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full
|
powershell.exe -NoProfile -File ".opencode/skills/db-dump-xml/scripts/db-dump-xml.ps1" -V8Path "C:\Program Files\1cv8\8.3.25.1257\bin" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full
|
||||||
|
|
||||||
# Инкрементальная выгрузка
|
# Инкрементальная выгрузка
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Changes
|
powershell.exe -NoProfile -File ".opencode/skills/db-dump-xml/scripts/db-dump-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Changes
|
||||||
|
|
||||||
# Частичная выгрузка
|
# Частичная выгрузка
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Partial -Objects "Справочник.Номенклатура,Документ.Заказ"
|
powershell.exe -NoProfile -File ".opencode/skills/db-dump-xml/scripts/db-dump-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Partial -Objects "Справочник.Номенклатура,Документ.Заказ"
|
||||||
|
|
||||||
# Серверная база
|
# Серверная база
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -ConfigDir "C:\WS\cfsrc" -Mode Full
|
powershell.exe -NoProfile -File ".opencode/skills/db-dump-xml/scripts/db-dump-xml.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -ConfigDir "C:\WS\cfsrc" -Mode Full
|
||||||
|
|
||||||
# Выгрузка расширения
|
# Выгрузка расширения
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\ext_src" -Mode Full -Extension "МоёРасширение"
|
powershell.exe -NoProfile -File ".opencode/skills/db-dump-xml/scripts/db-dump-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\ext_src" -Mode Full -Extension "МоёРасширение"
|
||||||
```
|
```
|
||||||
@@ -36,7 +36,7 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.ps1" <параметры>
|
powershell.exe -NoProfile -File ".opencode/skills/db-load-cf/scripts/db-load-cf.ps1" <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
@@ -63,11 +63,11 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.ps1" <п
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Файловая база
|
# Файловая база
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "C:\backup\config.cf"
|
powershell.exe -NoProfile -File ".opencode/skills/db-load-cf/scripts/db-load-cf.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "C:\backup\config.cf"
|
||||||
|
|
||||||
# Серверная база
|
# Серверная база
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test" -UserName "Admin" -Password "secret" -InputFile "config.cf"
|
powershell.exe -NoProfile -File ".opencode/skills/db-load-cf/scripts/db-load-cf.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test" -UserName "Admin" -Password "secret" -InputFile "config.cf"
|
||||||
|
|
||||||
# Загрузка расширения
|
# Загрузка расширения
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "ext.cfe" -Extension "МоёРасширение"
|
powershell.exe -NoProfile -File ".opencode/skills/db-load-cf/scripts/db-load-cf.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "ext.cfe" -Extension "МоёРасширение"
|
||||||
```
|
```
|
||||||
@@ -52,7 +52,7 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-dt.ps1" <параметры>
|
powershell.exe -NoProfile -File ".opencode/skills/db-load-dt/scripts/db-load-dt.ps1" <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
@@ -80,10 +80,10 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-dt.ps1" <п
|
|||||||
|
|
||||||
```powershell
|
```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 ".opencode/skills/db-load-dt/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
|
powershell.exe -NoProfile -File ".opencode/skills/db-load-dt/scripts/db-load-dt.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test" -UserName "Admin" -Password "secret" -InputFile "base.dt" -JobsCount 4
|
||||||
```
|
```
|
||||||
|
|
||||||
## Связанные навыки
|
## Связанные навыки
|
||||||
@@ -38,7 +38,7 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-git.ps1" <параметры>
|
powershell.exe -NoProfile -File ".opencode/skills/db-load-git/scripts/db-load-git.ps1" <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
@@ -70,8 +70,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-git.ps1" <
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Все незафиксированные изменения
|
# Все незафиксированные изменения
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-git.ps1" -V8Path "C:\Program Files\1cv8\8.3.25.1257\bin" -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\WS\cfsrc" -Source All -UpdateDB
|
powershell.exe -NoProfile -File ".opencode/skills/db-load-git/scripts/db-load-git.ps1" -V8Path "C:\Program Files\1cv8\8.3.25.1257\bin" -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\WS\cfsrc" -Source All -UpdateDB
|
||||||
|
|
||||||
# Из диапазона коммитов
|
# Из диапазона коммитов
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-git.ps1" -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\WS\cfsrc" -Source Commit -CommitRange "HEAD~3..HEAD"
|
powershell.exe -NoProfile -File ".opencode/skills/db-load-git/scripts/db-load-git.ps1" -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\WS\cfsrc" -Source Commit -CommitRange "HEAD~3..HEAD"
|
||||||
```
|
```
|
||||||
@@ -38,7 +38,7 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" <параметры>
|
powershell.exe -NoProfile -File ".opencode/skills/db-load-xml/scripts/db-load-xml.ps1" <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
@@ -88,14 +88,14 @@ Documents/Заказ/Forms/ФормаДокумента.xml
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Полная загрузка
|
# Полная загрузка
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" -V8Path "C:\Program Files\1cv8\8.3.25.1257\bin" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full
|
powershell.exe -NoProfile -File ".opencode/skills/db-load-xml/scripts/db-load-xml.ps1" -V8Path "C:\Program Files\1cv8\8.3.25.1257\bin" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full
|
||||||
|
|
||||||
# Частичная загрузка конкретных файлов
|
# Частичная загрузка конкретных файлов
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Partial -Files "Catalogs/Номенклатура.xml,Catalogs/Номенклатура/Ext/ObjectModule.bsl"
|
powershell.exe -NoProfile -File ".opencode/skills/db-load-xml/scripts/db-load-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Partial -Files "Catalogs/Номенклатура.xml,Catalogs/Номенклатура/Ext/ObjectModule.bsl"
|
||||||
|
|
||||||
# Загрузка расширения
|
# Загрузка расширения
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\ext_src" -Mode Full -Extension "МоёРасширение"
|
powershell.exe -NoProfile -File ".opencode/skills/db-load-xml/scripts/db-load-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\ext_src" -Mode Full -Extension "МоёРасширение"
|
||||||
|
|
||||||
# Загрузка + обновление БД в одном запуске
|
# Загрузка + обновление БД в одном запуске
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full -UpdateDB
|
powershell.exe -NoProfile -File ".opencode/skills/db-load-xml/scripts/db-load-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full -UpdateDB
|
||||||
```
|
```
|
||||||
@@ -36,7 +36,7 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" <параметры>
|
powershell.exe -NoProfile -File ".opencode/skills/db-run/scripts/db-run.ps1" <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
@@ -63,14 +63,14 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" <пар
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Простой запуск
|
# Простой запуск
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin"
|
powershell.exe -NoProfile -File ".opencode/skills/db-run/scripts/db-run.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin"
|
||||||
|
|
||||||
# Запуск с обработкой
|
# Запуск с обработкой
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -Execute "C:\epf\МояОбработка.epf"
|
powershell.exe -NoProfile -File ".opencode/skills/db-run/scripts/db-run.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -Execute "C:\epf\МояОбработка.epf"
|
||||||
|
|
||||||
# Открыть по навигационной ссылке
|
# Открыть по навигационной ссылке
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -URL "e1cib/data/Справочник.Номенклатура"
|
powershell.exe -NoProfile -File ".opencode/skills/db-run/scripts/db-run.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -URL "e1cib/data/Справочник.Номенклатура"
|
||||||
|
|
||||||
# Серверная база с параметром запуска
|
# Серверная база с параметром запуска
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -CParam "ЗапуститьОбновление"
|
powershell.exe -NoProfile -File ".opencode/skills/db-run/scripts/db-run.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -CParam "ЗапуститьОбновление"
|
||||||
```
|
```
|
||||||
@@ -35,7 +35,7 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" <параметры>
|
powershell.exe -NoProfile -File ".opencode/skills/db-update/scripts/db-update.ps1" <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
@@ -76,11 +76,11 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" <п
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Обычное обновление (файловая база)
|
# Обычное обновление (файловая база)
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin"
|
powershell.exe -NoProfile -File ".opencode/skills/db-update/scripts/db-update.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin"
|
||||||
|
|
||||||
# Динамическое обновление (серверная база)
|
# Динамическое обновление (серверная база)
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -Dynamic "+"
|
powershell.exe -NoProfile -File ".opencode/skills/db-update/scripts/db-update.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -Dynamic "+"
|
||||||
|
|
||||||
# Обновление расширения
|
# Обновление расширения
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -Extension "МоёРасширение"
|
powershell.exe -NoProfile -File ".opencode/skills/db-update/scripts/db-update.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -Extension "МоёРасширение"
|
||||||
```
|
```
|
||||||
@@ -40,7 +40,7 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-build.ps1" <параметры>
|
powershell.exe -NoProfile -File ".opencode/skills/epf-build/scripts/epf-build.ps1" <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
@@ -62,8 +62,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-build.ps1" <п
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Сборка обработки (файловая база)
|
# Сборка обработки (файловая база)
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-build.ps1" -InfoBasePath "C:\Bases\MyDB" -SourceFile "src/МояОбработка.xml" -OutputFile "build/МояОбработка.epf"
|
powershell.exe -NoProfile -File ".opencode/skills/epf-build/scripts/epf-build.ps1" -InfoBasePath "C:\Bases\MyDB" -SourceFile "src/МояОбработка.xml" -OutputFile "build/МояОбработка.epf"
|
||||||
|
|
||||||
# Серверная база
|
# Серверная база
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-build.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -SourceFile "src/МояОбработка.xml" -OutputFile "build/МояОбработка.epf"
|
powershell.exe -NoProfile -File ".opencode/skills/epf-build/scripts/epf-build.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -SourceFile "src/МояОбработка.xml" -OutputFile "build/МояОбработка.epf"
|
||||||
```
|
```
|
||||||
@@ -39,7 +39,7 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-dump.ps1" <параметры>
|
powershell.exe -NoProfile -File ".opencode/skills/epf-dump/scripts/epf-dump.ps1" <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
@@ -62,8 +62,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-dump.ps1" <па
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Разборка обработки (файловая база)
|
# Разборка обработки (файловая база)
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-dump.ps1" -InfoBasePath "C:\Bases\MyDB" -InputFile "build/МояОбработка.epf" -OutputDir "src"
|
powershell.exe -NoProfile -File ".opencode/skills/epf-dump/scripts/epf-dump.ps1" -InfoBasePath "C:\Bases\MyDB" -InputFile "build/МояОбработка.epf" -OutputDir "src"
|
||||||
|
|
||||||
# Серверная база
|
# Серверная база
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-dump.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -InputFile "build/МояОбработка.epf" -OutputDir "src"
|
powershell.exe -NoProfile -File ".opencode/skills/epf-dump/scripts/epf-dump.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -InputFile "build/МояОбработка.epf" -OutputDir "src"
|
||||||
```
|
```
|
||||||
@@ -30,7 +30,7 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/init.ps1" -Name "<Name>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"]
|
powershell.exe -NoProfile -File ".opencode/skills/epf-init/scripts/init.ps1" -Name "<Name>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"]
|
||||||
```
|
```
|
||||||
|
|
||||||
## Дальнейшие шаги
|
## Дальнейшие шаги
|
||||||
@@ -24,7 +24,7 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-validate.ps1" -ObjectPath "src/МояОбработка"
|
powershell.exe -NoProfile -File ".opencode/skills/epf-validate/scripts/epf-validate.ps1" -ObjectPath "src/МояОбработка"
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-validate.ps1" -ObjectPath "src/МояОбработка/МояОбработка.xml"
|
powershell.exe -NoProfile -File ".opencode/skills/epf-validate/scripts/epf-validate.ps1" -ObjectPath "src/МояОбработка/МояОбработка.xml"
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ allowed-tools:
|
|||||||
Используй общий скрипт из epf-build:
|
Используй общий скрипт из epf-build:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-build/scripts/epf-build.ps1" <параметры>
|
powershell.exe -NoProfile -File ".opencode/skills/epf-build/scripts/epf-build.ps1" <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
@@ -64,8 +64,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-build/scripts/epf-bu
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Сборка отчёта (файловая база)
|
# Сборка отчёта (файловая база)
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-build/scripts/epf-build.ps1" -InfoBasePath "C:\Bases\MyDB" -SourceFile "src/МойОтчёт.xml" -OutputFile "build/МойОтчёт.erf"
|
powershell.exe -NoProfile -File ".opencode/skills/epf-build/scripts/epf-build.ps1" -InfoBasePath "C:\Bases\MyDB" -SourceFile "src/МойОтчёт.xml" -OutputFile "build/МойОтчёт.erf"
|
||||||
|
|
||||||
# Серверная база
|
# Серверная база
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-build/scripts/epf-build.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -SourceFile "src/МойОтчёт.xml" -OutputFile "build/МойОтчёт.erf"
|
powershell.exe -NoProfile -File ".opencode/skills/epf-build/scripts/epf-build.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -SourceFile "src/МойОтчёт.xml" -OutputFile "build/МойОтчёт.erf"
|
||||||
```
|
```
|
||||||
@@ -41,7 +41,7 @@ allowed-tools:
|
|||||||
Используй общий скрипт из epf-dump:
|
Используй общий скрипт из epf-dump:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-dump/scripts/epf-dump.ps1" <параметры>
|
powershell.exe -NoProfile -File ".opencode/skills/epf-dump/scripts/epf-dump.ps1" <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
@@ -64,8 +64,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-dump/scripts/epf-dum
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Разборка отчёта (файловая база)
|
# Разборка отчёта (файловая база)
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-dump/scripts/epf-dump.ps1" -InfoBasePath "C:\Bases\MyDB" -InputFile "build/МойОтчёт.erf" -OutputDir "src"
|
powershell.exe -NoProfile -File ".opencode/skills/epf-dump/scripts/epf-dump.ps1" -InfoBasePath "C:\Bases\MyDB" -InputFile "build/МойОтчёт.erf" -OutputDir "src"
|
||||||
|
|
||||||
# Серверная база
|
# Серверная база
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-dump/scripts/epf-dump.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -InputFile "build/МойОтчёт.erf" -OutputDir "src"
|
powershell.exe -NoProfile -File ".opencode/skills/epf-dump/scripts/epf-dump.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -InputFile "build/МойОтчёт.erf" -OutputDir "src"
|
||||||
```
|
```
|
||||||
@@ -31,7 +31,7 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/init.ps1" -Name "<Name>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"] [-WithSKD]
|
powershell.exe -NoProfile -File ".opencode/skills/erf-init/scripts/init.ps1" -Name "<Name>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"] [-WithSKD]
|
||||||
```
|
```
|
||||||
|
|
||||||
## Дальнейшие шаги
|
## Дальнейшие шаги
|
||||||
@@ -26,7 +26,7 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-validate/scripts/epf-validate.ps1" -ObjectPath "src/МойОтчёт"
|
powershell.exe -NoProfile -File ".opencode/skills/epf-validate/scripts/epf-validate.ps1" -ObjectPath "src/МойОтчёт"
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-validate/scripts/epf-validate.ps1" -ObjectPath "src/МойОтчёт/МойОтчёт.xml"
|
powershell.exe -NoProfile -File ".opencode/skills/epf-validate/scripts/epf-validate.ps1" -ObjectPath "src/МойОтчёт/МойОтчёт.xml"
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-add.ps1" -ObjectPath "<ObjectPath>" -FormName "<FormName>" [-Purpose "<Purpose>"] [-Synonym "<Synonym>"] [-SetDefault]
|
powershell.exe -NoProfile -File ".opencode/skills/form-add/scripts/form-add.ps1" -ObjectPath "<ObjectPath>" -FormName "<FormName>" [-Purpose "<Purpose>"] [-Synonym "<Synonym>"] [-SetDefault]
|
||||||
```
|
```
|
||||||
|
|
||||||
## Purpose — назначение формы
|
## Purpose — назначение формы
|
||||||
+14
-1
@@ -1,4 +1,4 @@
|
|||||||
# form-add v1.8 — Add managed form to 1C config object
|
# form-add v1.9 — Add managed form to 1C config object
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
@@ -33,6 +33,16 @@ function Get-RootUuid([string]$xmlPath) {
|
|||||||
} catch {}
|
} catch {}
|
||||||
return $null
|
return $null
|
||||||
}
|
}
|
||||||
|
function Test-ExternalObjectRoot([string]$xmlPath) {
|
||||||
|
if (-not (Test-Path $xmlPath)) { return $false }
|
||||||
|
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) { return @('ExternalDataProcessor','ExternalReport') -contains $el.LocalName }
|
||||||
|
} catch {}
|
||||||
|
return $false
|
||||||
|
}
|
||||||
function Find-V8Project([string]$startDir) {
|
function Find-V8Project([string]$startDir) {
|
||||||
$d = $startDir
|
$d = $startDir
|
||||||
for ($i = 0; $i -lt 20 -and $d; $i++) {
|
for ($i = 0; $i -lt 20 -and $d; $i++) {
|
||||||
@@ -69,10 +79,13 @@ function Assert-EditAllowed([string]$targetPath, [string]$require) {
|
|||||||
try {
|
try {
|
||||||
$rp = $targetPath
|
$rp = $targetPath
|
||||||
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
|
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
|
||||||
|
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
|
||||||
|
if (Test-ExternalObjectRoot $rp) { return }
|
||||||
$elemUuid = Get-RootUuid $rp
|
$elemUuid = Get-RootUuid $rp
|
||||||
$cfgDir = $null; $binPath = $null
|
$cfgDir = $null; $binPath = $null
|
||||||
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
|
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
|
||||||
for ($i = 0; $i -lt 12 -and $d; $i++) {
|
for ($i = 0; $i -lt 12 -and $d; $i++) {
|
||||||
|
if (Test-ExternalObjectRoot "$d.xml") { return }
|
||||||
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
|
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
|
||||||
if (-not $cfgDir) {
|
if (-not $cfgDir) {
|
||||||
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
|
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user