mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-06-26 15:04:34 +03:00
378b19b59f
- support-guard: вместо общего списка всех вариантов — текст под конкретную причину отказа (decideSupport.code: capability-off | locked | not-removed), с подставленным реальным путём и точными командами support-edit. Понятно модели вне контекста: что за состояние и что именно сделать. - support-state: decideSupport возвращает code (дискриминатор причины). - README: переписан для читателя — убраны отсылки к внутренней реализации (§-нумерация, декодер, разбор common/); назначение, установка, настройка (.v8-project.json), что делать при отказе, проверка. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
122 lines
6.3 KiB
JavaScript
122 lines
6.3 KiB
JavaScript
// support-guard.mjs v1.0 — PreToolUse hook (§1A): block raw Edit/Write/MultiEdit of
|
|
// vendor objects "на замке" / read-only configs that bypass the in-skill guard (§1B).
|
|
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
//
|
|
// stdin: PreToolUse JSON { tool_name, tool_input, cwd, ... }.
|
|
// Decision via stdout JSON hookSpecificOutput.permissionDecision (deny) — see
|
|
// docs/1c-support-state-spec.md. Reaction (deny|warn|off) from .v8-project.json
|
|
// editingAllowedCheck, identical to §1B. Never blocks on its own errors.
|
|
|
|
import { decideSupport } from './common/support-state.mjs';
|
|
import { getEditMode } from './common/project.mjs';
|
|
import { resolve, isAbsolute } from 'node:path';
|
|
|
|
// Collect candidate file paths from an Edit/Write/MultiEdit tool_input. Handles the
|
|
// single-file form ({ file_path }) and the array form ({ file_edits: [{ file_path }] }).
|
|
function candidatePaths(toolInput) {
|
|
const out = [];
|
|
if (!toolInput || typeof toolInput !== 'object') return out;
|
|
if (typeof toolInput.file_path === 'string') out.push(toolInput.file_path);
|
|
if (Array.isArray(toolInput.file_edits)) {
|
|
for (const e of toolInput.file_edits) {
|
|
if (e && typeof e.file_path === 'string') out.push(e.file_path);
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
// Tailored, model-actionable diagnostic per block cause (see decideSupport `code`).
|
|
// Fills in the real target/config paths so the suggested commands are ready to run.
|
|
function diagnostic(code, target, cfgDir) {
|
|
const head =
|
|
'[support-guard] Правка отклонена: это объект типовой конфигурации на поддержке поставщика, ' +
|
|
'прямая правка молча сломает будущие обновления.';
|
|
const cfe =
|
|
'Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — ' +
|
|
'состояние поддержки менять не нужно, обновления вендора сохраняются.';
|
|
const offNote = 'Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json.';
|
|
const root = cfgDir || '<каталог дампа>';
|
|
|
|
if (code === 'capability-off') {
|
|
return [
|
|
head,
|
|
`Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — ` +
|
|
`поэтому объект «${target}» править нельзя.`,
|
|
cfe,
|
|
`Либо снять защиту явно (навык support-edit, два шага):`,
|
|
` 1. support-edit -Path "${root}" -Capability on — включить возможность изменения (объекты пока остаются на замке);`,
|
|
` 2. support-edit -Path "${target}" -Set editable — открыть этот объект для правки.`,
|
|
`Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора.`,
|
|
offNote,
|
|
].join('\n');
|
|
}
|
|
if (code === 'not-removed') {
|
|
return [
|
|
head,
|
|
`Состояние: объект «${target}» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора.`,
|
|
cfe,
|
|
`Либо сначала снять объект с поддержки, затем удалять:`,
|
|
` support-edit -Path "${target}" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно.`,
|
|
offNote,
|
|
].join('\n');
|
|
}
|
|
// locked (G=0, f1=0)
|
|
return [
|
|
head,
|
|
`Состояние: объект «${target}» на замке (возможность изменения конфигурации включена, но сам объект не редактируется).`,
|
|
cfe,
|
|
`Либо разрешить правку этого объекта (навык support-edit, выбрать одно):`,
|
|
` • support-edit -Path "${target}" -Set editable — править и дальше получать обновления вендора (при обновлении возможны конфликты слияния);`,
|
|
` • support-edit -Path "${target}" -Set off-support — снять с поддержки: правки свободны, обновления по объекту больше не приходят.`,
|
|
offNote,
|
|
].join('\n');
|
|
}
|
|
|
|
// Core decision. Returns { stdout, stderr, exitCode }. Pure (no I/O) for testability.
|
|
export function processInput(input) {
|
|
const empty = { stdout: '', stderr: '', exitCode: 0 };
|
|
try {
|
|
const cwd = typeof input.cwd === 'string' ? input.cwd : process.cwd();
|
|
const paths = candidatePaths(input.tool_input);
|
|
for (const p of paths) {
|
|
const target = isAbsolute(p) ? p : resolve(cwd, p);
|
|
const r = decideSupport(target, 'editable');
|
|
if (!r.blocked) continue;
|
|
const mode = getEditMode(r.cfgDir, cwd);
|
|
if (mode === 'off') continue;
|
|
if (mode === 'warn') {
|
|
return { stdout: '', stderr: `[support-guard] ПРЕДУПРЕЖДЕНИЕ: ${r.reason}. Цель: ${target}`, exitCode: 0 };
|
|
}
|
|
// deny (default): structured PreToolUse decision.
|
|
const decision = {
|
|
hookSpecificOutput: {
|
|
hookEventName: 'PreToolUse',
|
|
permissionDecision: 'deny',
|
|
permissionDecisionReason: diagnostic(r.code, target, r.cfgDir),
|
|
},
|
|
};
|
|
return { stdout: JSON.stringify(decision), stderr: '', exitCode: 0 };
|
|
}
|
|
return empty;
|
|
} catch {
|
|
return empty; // guard errors must never block
|
|
}
|
|
}
|
|
|
|
async function readStdin() {
|
|
const chunks = [];
|
|
for await (const c of process.stdin) chunks.push(c);
|
|
return Buffer.concat(chunks).toString('utf8');
|
|
}
|
|
|
|
// Run only when executed directly (not when imported by tests).
|
|
if (import.meta.url === `file://${process.argv[1]}` || process.argv[1]?.endsWith('support-guard.mjs')) {
|
|
const raw = await readStdin();
|
|
let input = {};
|
|
try { input = raw.trim() ? JSON.parse(raw) : {}; } catch { input = {}; }
|
|
const { stdout, stderr, exitCode } = processInput(input);
|
|
if (stdout) process.stdout.write(stdout);
|
|
if (stderr) process.stderr.write(stderr + '\n');
|
|
process.exit(exitCode);
|
|
}
|