#!/usr/bin/env node // build-webtest-db v0.2 — Собирает синтетическую web-test конфигурацию в постоянные пути // и накатывает её в зарегистрированную базу `webtest` (см. .v8-project.json). // // Двойной режим: // - CLI: node tests/skills/build-webtest-db.mjs [--runtime ...] [--skip-platform] // - Module: import { runSteps, execSkill, getProjectInfo, ... } from './build-webtest-db.mjs' // // CLI: // node tests/skills/build-webtest-db.mjs # пересобрать с нуля // node tests/skills/build-webtest-db.mjs --runtime python // node tests/skills/build-webtest-db.mjs --skip-platform # только XML, без db-create/load/update // // После завершения база готова к /web-publish + web-test сессии. import { execFile } from 'child_process'; import { existsSync, mkdirSync, rmSync, readFileSync, writeFileSync } from 'fs'; import { join, resolve, dirname } from 'path'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const ROOT = dirname(__filename); const REPO_ROOT = resolve(ROOT, '../..'); const SKILLS = resolve(REPO_ROOT, '.claude/skills'); // ── Public API ──────────────────────────────────────────────────────────────── /** * Reads .v8-project.json and locates webtest registration. * @returns {{ v8path: string, v8exe: string, webtestDb: object, configSrc: string, dbPath: string }} */ export function getProjectInfo() { const projectFile = join(REPO_ROOT, '.v8-project.json'); if (!existsSync(projectFile)) throw new Error('.v8-project.json not found'); const proj = JSON.parse(readFileSync(projectFile, 'utf8')); const webtestDb = proj.databases?.find(d => d.id === 'webtest'); if (!webtestDb) throw new Error('Database "webtest" not registered in .v8-project.json'); const v8path = proj.v8path; const v8exe = join(v8path, '1cv8.exe'); const dbPath = webtestDb.path; const configSrc = resolve(REPO_ROOT, webtestDb.configSrc); return { v8path, v8exe, webtestDb, configSrc, dbPath }; } /** * Resolves a skill script path to an absolute file (chooses .ps1 or .py based on runtime). */ export function resolveScript(scriptRelPath, runtime = 'powershell') { const ext = runtime === 'python' ? '.py' : '.ps1'; const full = join(SKILLS, scriptRelPath + ext); if (!existsSync(full)) throw new Error(`Script not found: ${full}`); return full; } /** * Executes a single skill script with provided arguments. * @returns {Promise} stdout */ export function execSkill(scriptPath, args, runtime = 'powershell') { return new Promise((res, rej) => { const cmd = runtime === 'python' ? [process.env.PYTHON || 'python', [scriptPath, ...args]] : ['powershell.exe', ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File', scriptPath, ...args]]; execFile(cmd[0], cmd[1], { encoding: 'utf8', timeout: 120_000, cwd: REPO_ROOT }, (err, stdout, stderr) => { if (err) { rej(new Error(stderr?.trim() || stdout?.trim() || err.message)); } else { res(stdout); } }); }); } /** * Replaces {workDir}/{v8path}/{dbPath} placeholders in a string value. */ export function replacePlaceholders(s, paths) { return String(s) .replace('{workDir}', paths.workDir ?? '') .replace('{v8path}', paths.v8path ?? '') .replace('{dbPath}', paths.dbPath ?? ''); } /** * Executes an array of build steps. * * Each step: { name, script?, args?, input?, writeFile?, content? } * - writeFile: write content to a file (relative to workDir or absolute), skip script call * - script: relative path under .claude/skills (without extension) * - args: { '-Flag': value | true }, value may contain {workDir}/{v8path}/{dbPath}/{inputFile} * - input: JSON object written to __input.json (referenced by {inputFile} in args) * * @param {Array} steps * @param {{ workDir: string, v8path: string, dbPath: string }} paths * @param {string} runtime 'powershell' | 'python' * @param {(line: string) => void} log * @returns {Promise<{ ok: boolean, elapsed: number, failedAt?: number }>} */ export async function runSteps(steps, paths, runtime, log = console.log) { const t0 = Date.now(); for (let i = 0; i < steps.length; i++) { const step = steps[i]; const stepT0 = Date.now(); if (step.writeFile) { try { const target = replacePlaceholders(step.writeFile, paths); const abs = target.includes(':') || target.startsWith('/') ? target : join(paths.workDir, target); mkdirSync(dirname(abs), { recursive: true }); writeFileSync(abs, step.content ?? '', 'utf8'); const ms = Date.now() - stepT0; log(` [${i + 1}/${steps.length}] OK ${step.name} (${(ms / 1000).toFixed(1)}s)`); } catch (e) { log(` [${i + 1}/${steps.length}] FAIL ${step.name}: ${e.message}`); return { ok: false, elapsed: (Date.now() - t0) / 1000, failedAt: i }; } continue; } let inputFile = null; if (step.input) { inputFile = join(paths.workDir, '__input.json'); writeFileSync(inputFile, JSON.stringify(step.input, null, 2), 'utf8'); } const script = resolveScript(step.script, runtime); const args = []; for (const [flag, value] of Object.entries(step.args || {})) { args.push(flag); if (value === true) continue; let v = String(value).replace('{inputFile}', inputFile || ''); v = replacePlaceholders(v, paths); args.push(v); } try { await execSkill(script, args, runtime); if (inputFile && existsSync(inputFile)) rmSync(inputFile); const ms = Date.now() - stepT0; log(` [${i + 1}/${steps.length}] OK ${step.name} (${(ms / 1000).toFixed(1)}s)`); } catch (e) { if (inputFile && existsSync(inputFile)) rmSync(inputFile); log(` [${i + 1}/${steps.length}] FAIL ${step.name}`); log(` ${e.message.split('\n').join('\n ').substring(0, 1500)}`); return { ok: false, elapsed: (Date.now() - t0) / 1000, failedAt: i }; } } return { ok: true, elapsed: (Date.now() - t0) / 1000 }; } /** * Returns the standard platform load steps (db-create + db-load-xml + db-update). */ export function platformLoadSteps() { return [ { name: 'db-create: создание файловой ИБ', script: 'db-create/scripts/db-create', args: { '-V8Path': '{v8path}', '-InfoBasePath': '{dbPath}' }, }, { name: 'db-load-xml: загрузка конфигурации', script: 'db-load-xml/scripts/db-load-xml', args: { '-V8Path': '{v8path}', '-InfoBasePath': '{dbPath}', '-ConfigDir': '{workDir}' }, }, { name: 'db-update: обновление БД', script: 'db-update/scripts/db-update', args: { '-V8Path': '{v8path}', '-InfoBasePath': '{dbPath}' }, }, ]; } /** * Imports the build-webtest-config.test.mjs steps array. */ export async function loadBuildSteps() { const buildModule = await import(`file://${join(ROOT, 'integration/build-webtest-config.test.mjs').replace(/\\/g, '/')}`); return buildModule.steps; } // ── CLI ──────────────────────────────────────────────────────────────────────── async function runCli() { const argv = process.argv.slice(2); const opts = { runtime: 'powershell', skipPlatform: false }; for (let i = 0; i < argv.length; i++) { const a = argv[i]; if (a === '--runtime' && argv[i + 1]) { opts.runtime = argv[++i]; continue; } if (a === '--skip-platform') { opts.skipPlatform = true; continue; } if (a === '-h' || a === '--help') { console.log('Usage: build-webtest-db.mjs [--runtime powershell|python] [--skip-platform]'); process.exit(0); } } const { v8path, v8exe, configSrc, dbPath } = getProjectInfo(); if (!opts.skipPlatform && !existsSync(v8exe)) { console.error(`1cv8.exe not found at ${v8exe}`); process.exit(1); } console.log(`[build-webtest-db] configSrc: ${configSrc}`); console.log(`[build-webtest-db] dbPath: ${dbPath}`); console.log(`[build-webtest-db] runtime: ${opts.runtime}`); console.log(''); if (existsSync(configSrc)) { console.log(`Removing existing configSrc...`); rmSync(configSrc, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 }); } mkdirSync(configSrc, { recursive: true }); if (!opts.skipPlatform && existsSync(dbPath)) { console.log(`Removing existing IB...`); rmSync(dbPath, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 }); } const buildSteps = await loadBuildSteps(); const platformSteps = opts.skipPlatform ? [] : platformLoadSteps(); const allSteps = [...buildSteps, ...platformSteps]; const paths = { workDir: configSrc, v8path, dbPath }; const result = await runSteps(allSteps, paths, opts.runtime, console.log); console.log(''); if (!result.ok) { console.error(`Build FAILED after ${result.elapsed.toFixed(1)}s`); process.exit(1); } console.log(`Build OK (${result.elapsed.toFixed(1)}s)`); console.log(''); console.log(` configSrc: ${configSrc}`); if (!opts.skipPlatform) { console.log(` IB: ${dbPath}`); console.log(''); console.log(` Next: /web-publish webtest → open in browser`); } } // CLI guard: run only when invoked directly, not when imported. const invokedDirectly = process.argv[1] ? fileURLToPath(import.meta.url) === resolve(process.argv[1]) : false; if (invokedDirectly) { runCli().catch(e => { console.error(e.message); process.exit(1); }); }